npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

nextsheet

v0.5.0

Published

The framework for the spreadsheet era.

Readme

nextsheet

The framework for the spreadsheet era.

Build spreadsheets as typed React-like components. One codebase compiles to CSV, Excel, Google Sheets, and any backend you need. Now with a first-class Agent API for LLM-driven workbook manipulation.

npm install nextsheet
# or
pnpm add nextsheet

Quick start

1. Configure TypeScript

{
  "compilerOptions": {
    "jsx": "react-jsx",
    "jsxImportSource": "nextsheet"
  }
}

2. Write a sheet

// Invoices.sheet.tsx
import { defineSheet, Sheet, Column, Formula } from 'nextsheet'

export default defineSheet('Invoices 2026', () => (
  <Sheet>
    <Column name="id"     type="number"   primary />
    <Column name="client" type="string"   required />
    <Column name="amount" type="currency" currency="USD" />
    <Column name="paid"   type="boolean"  default={false} />
    <Column name="total"  type="currency"
      formula={({ amount }) => <Formula>{amount} * 1.21</Formula>}
    />
  </Sheet>
))

3. Render it

import { workbook, csvAdapter, xlsxAdapter } from 'nextsheet'
import Invoices from './Invoices.sheet.js'

const wb = workbook('Finance 2026', [Invoices])

const csv  = await csvAdapter.render(wb)   // → string
const xlsx = await xlsxAdapter.render(wb)  // → Buffer

Or use the CLI:

npx nextsheet build Invoices.sheet.tsx --target xlsx --out dist/invoices.xlsx
npx nextsheet dev   Invoices.sheet.tsx   # live browser preview at localhost:3000

Agent API

The nextsheet/agent entrypoint gives LLMs a structured way to create and edit workbooks — without string parsing, brittle prompts, or custom schemas.

import {
  wb, fromWorkbook,
  toAnthropicTools, toOpenAITools,
  validatePatch, validateWorkbook, validatePatches,
  AgentError,
} from 'nextsheet/agent'

Build a workbook programmatically

import { wb } from 'nextsheet/agent'
import { xlsxAdapter } from 'nextsheet'

const builder = wb('Q1 Finance')
  .sheet('Revenue', (s) => {
    s.column('Month',   'string',   { primary: true })
     .column('Revenue', 'currency', { currency: 'USD' })
     .column('Growth',  'percent')
     .row(['Jan', 420_000, 0.08])
     .row(['Feb', 510_000, 0.21])
     .row(['Mar', 480_000, -0.06])
  })

const node = builder.build()
const xlsx = await xlsxAdapter.render(node)

Edit an existing workbook with patches

Use applyPatch() to apply one or more operations atomically. Great for applying LLM-generated edits to a workbook the user already has open.

import { fromWorkbook } from 'nextsheet/agent'

const builder = fromWorkbook(existingNode)   // deep-copies the node

builder.applyPatch(
  { op: 'addRow',    sheet: 'Revenue', values: { Month: 'Apr', Revenue: 560_000, Growth: 0.17 } },
  { op: 'updateCell', sheet: 'Revenue', rowIndex: 0, column: 'Revenue', value: 425_000 },
  { op: 'addColumn', sheet: 'Revenue', name: 'Forecast', type: 'currency' },
)

const updated = builder.build()

Patch operations

| op | Required fields | Description | |---|---|---| | addRow | sheet, values | Append or insert a row. values maps column name → value. | | removeRow | sheet, rowIndex | Remove the row at a 0-based index. | | updateCell | sheet, rowIndex, column | Set value, formula, format, color, and/or bold on a cell. | | addColumn | sheet, name, type | Append or insert a column. Adds a null cell to every existing row. | | removeColumn | sheet, column | Remove a column and its cells from all rows. | | updateColumn | sheet, column, updates | Rename, retype, or change options on a column. | | setHeader | sheet, title | Set the sheet header title (and optional subtitle). | | addSheet | name | Add a blank sheet to the workbook. | | removeSheet | sheet | Remove a sheet by name. | | renameSheet | sheet, newName | Rename a sheet. | | addChart | sheet, chart | Append a chart to a sheet. |

Column types: string · number · currency · boolean · date · percent

Chart types: bar · horizontal-bar · line · area · stacked-bar · stacked-area · pie · donut · scatter · bubble · radar

Error handling

applyPatch() throws AgentError (a subclass of Error) on invalid operations. The code property is safe to switch on.

import { AgentError } from 'nextsheet/agent'

try {
  builder.applyPatch({ op: 'addRow', sheet: 'Typo', values: {} })
} catch (e) {
  if (e instanceof AgentError) {
    console.error(e.code)   // 'SHEET_NOT_FOUND'
    console.error(e.op)     // 'addRow'
    console.error(e.message) // '[SHEET_NOT_FOUND] addRow: sheet "Typo" does not exist'
  }
}

| Code | Trigger | |---|---| | SHEET_NOT_FOUND | sheet references a name that doesn't exist | | SHEET_ALREADY_EXISTS | addSheet / renameSheet targets an existing name | | COLUMN_NOT_FOUND | column references a name not in the sheet | | COLUMN_ALREADY_EXISTS | addColumn / updateColumn rename uses a name already in use | | ROW_OUT_OF_BOUNDS | rowIndex is negative or ≥ rows.length | | EMPTY_UPDATE | updateColumn called with an empty updates object |

Validation

Validate LLM output before passing it to applyPatch() to avoid runtime errors.

import { validatePatch, validateWorkbook, validatePatches } from 'nextsheet/agent'

// Single patch
const r = validatePatch(agentOutput)
if (!r.valid) {
  console.error(r.errors)  // ['op: must be equal to one of the allowed values', ...]
} else {
  builder.applyPatch(agentOutput as PatchOperation)
}

// Array of patches — returns index of first invalid one
const r2 = validatePatches(agentPatches)
if (!r2.valid) {
  throw new Error(`Patch[${r2.index}] is invalid: ${r2.errors.join(', ')}`)
}

// Whole workbook (from create_workbook tool)
const r3 = validateWorkbook(toolCallInput)
if (r3.valid) {
  const node = toolCallInput as WorkbookNode
}

LLM tool definitions

Pass tool schemas directly to the Anthropic or OpenAI API:

import { toAnthropicTools, toOpenAITools } from 'nextsheet/agent'
import Anthropic from '@anthropic-ai/sdk'

const client = new Anthropic()

const response = await client.messages.create({
  model: 'claude-opus-4-7',
  max_tokens: 4096,
  tools: toAnthropicTools(),   // create_workbook + patch_workbook
  messages: [
    { role: 'user', content: 'Create a sales dashboard with monthly revenue data for Q1.' }
  ],
})
import { toOpenAITools } from 'nextsheet/agent'
import OpenAI from 'openai'

const openai = new OpenAI()

const completion = await openai.chat.completions.create({
  model: 'gpt-4o',
  tools: toOpenAITools(),
  messages: [{ role: 'user', content: 'Add a Forecast column to the Revenue sheet.' }],
})

The two built-in tools are:

| Tool | Input | Description | |---|---|---| | create_workbook | WorkbookNode JSON | Create a full workbook in one shot | | patch_workbook | { patches: PatchOperation[] } | Apply an ordered list of edits |

JSON Schemas

All schemas are exported for use in your own tool definitions or JSON Schema validators:

import {
  workbookSchema, sheetSchema, columnSchema, rowSchema, cellSchema,
  patchSchema,
  addRowSchema, updateCellSchema, removeRowSchema,
  addColumnSchema, removeColumnSchema, updateColumnSchema,
  setHeaderSchema,
  addSheetSchema, removeSheetSchema, renameSheetSchema,
  addChartSchema,
} from 'nextsheet/agent'

Components

Schema mode — defineSheet + Column

Best for structured data where each row follows the same shape.

import { defineSheet, Sheet, Column, Formula } from 'nextsheet'

export default defineSheet('Products', () => (
  <Sheet>
    <Column name="sku"      type="string"   required />
    <Column name="price"    type="currency" currency="EUR" />
    <Column name="stock"    type="number" />
    <Column name="value"    type="currency"
      formula={({ price, stock }) => <Formula>{price} * {stock}</Formula>}
    />
  </Sheet>
))

Column types: string · number · currency · boolean · date · percent

Report mode — function components

Best for dashboards, reports, and documents with mixed structure.

import { Sheet, Header, Section, Row, Cell, useFormula, useQuery } from 'nextsheet'

const data = [
  { region: 'North', revenue: 520_000, growth: 0.12 },
  { region: 'South', revenue: 310_000, growth: 0.08 },
]

export default function SalesReport() {
  const total = useFormula(() => data.reduce((s, r) => s + r.revenue, 0))
  const top   = useQuery(data).orderBy('revenue', 'desc').first()

  return (
    <Sheet name="Sales Report">
      <Header title="Q1 Sales" subtitle="January — March 2026" />

      <Section title="By region">
        <Row header>
          <Cell>Region</Cell>
          <Cell>Revenue</Cell>
          <Cell>Growth</Cell>
        </Row>
        {data.map((r) => (
          <Row key={r.region}>
            <Cell>{r.region}</Cell>
            <Cell format="currency">{r.revenue}</Cell>
            <Cell format="percent" color={r.growth > 0 ? 'green' : 'red'}>
              {r.growth}
            </Cell>
          </Row>
        ))}
      </Section>

      <Section title="Summary">
        <Row><Cell bold>Total</Cell><Cell format="currency">{total}</Cell></Row>
        <Row><Cell bold>Top region</Cell><Cell>{top?.region}</Cell></Row>
      </Section>
    </Sheet>
  )
}

Multi-sheet workbooks

import { workbook, xlsxAdapter } from 'nextsheet'
import Invoices from './Invoices.sheet.js'
import Expenses from './Expenses.sheet.js'

const wb = workbook('Finance 2026', [Invoices, Expenses])
const { data } = await xlsxAdapter.render(wb)

Hooks

import { useFormula, useQuery, useRange } from 'nextsheet'

// Evaluates JS at build time. Transpiles to native formulas in v0.2.
const total = useFormula(() => rows.reduce((s, r) => s + r.amount, 0))

// Chainable in-memory query.
const top = useQuery(sales)
  .where('region', 'North')
  .orderBy('amount', 'desc')
  .first()

// Live data from a connected backend. Available in v0.3.
const live = useRange<Row>('Sheet1!A2:D')

Charts

<Chart> and <ChartSeries> are first-class sheet components. Place them anywhere inside <Sheet> — after sections, after columns, or mixed in between.

In dev mode (nextsheet dev) charts render as interactive Chart.js visualizations in the browser preview. The charts array is available on every SheetNode so custom adapters can produce native chart output.

Basic example

import { Sheet, Column, Section, Row, Cell, Chart, ChartSeries } from 'nextsheet'

const months   = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun']
const revenue  = [42_000, 58_000, 51_000, 67_000, 73_000, 88_000]
const expenses = [28_000, 33_000, 30_000, 38_000, 41_000, 45_000]

export default function Dashboard() {
  return (
    <Sheet name="Dashboard">
      <Column name="Month"    type="string"   primary />
      <Column name="Revenue"  type="currency" />
      <Column name="Expenses" type="currency" />

      <Section>
        {months.map((m, i) => (
          <Row key={m}>
            <Cell>{m}</Cell>
            <Cell>{revenue[i]}</Cell>
            <Cell>{expenses[i]}</Cell>
          </Row>
        ))}
      </Section>

      {/* Series pull values from the columns above automatically */}
      <Chart type="bar" title="Revenue vs Expenses" xAxis="Month">
        <ChartSeries name="Revenue"  column="Revenue"  color="#22c55e" />
        <ChartSeries name="Expenses" column="Expenses" color="#ef4444" />
      </Chart>
    </Sheet>
  )
}

Chart types

| type | Description | |---|---| | bar | Vertical grouped bars | | horizontal-bar | Horizontal bars — good for rankings | | line | Line with data points | | area | Filled line | | stacked-bar | Bars stacked on top of each other | | stacked-area | Stacked filled lines — good for composition over time | | pie | Circular slices | | donut | Pie with hollow center | | scatter | Points at (x, y) coordinates | | bubble | Points with variable radius (x, y, r) | | radar | Spider chart — good for multi-axis comparison |


<Chart> props

| Prop | Type | Default | Description | |---|---|---|---| | type | ChartType | required | Chart type from the table above | | title | string | — | Title displayed above the chart | | xAxis | string | — | Column name to use as x-axis / category labels | | showLegend | boolean | true | Show or hide the series legend | | height | number | 320 | Canvas height in pixels | | width | number | — | Max canvas width in pixels |

<ChartSeries> props

| Prop | Type | Default | Description | |---|---|---|---| | name | string | required | Label shown in the legend and tooltips | | column | string | — | Column name in the sheet to read values from | | data | number[] | — | Inline numeric array — used when column is not set | | points | ChartPoint[] | — | Explicit {x, y, r?} points for scatter / bubble | | color | string | auto | Hex color (#3b82f6) or CSS color name |

ChartPoint is { x: number; y: number; r?: number }.


Data sources

Column reference — reads values from the sheet rows at render time:

<ChartSeries name="Revenue" column="Revenue" />

Inline array — hardcoded values, no columns required:

<ChartSeries name="Forecast" data={[55_000, 62_000, 70_000, 80_000]} />

Point array — for scatter and bubble charts:

<ChartSeries name="Products" points={[
  { x: 10, y: 4.2 },
  { x: 25, y: 3.8, r: 10 },
  { x: 50, y: 4.7, r: 6 },
]} />

Adapters

Built-in adapters

import { csvAdapter, xlsxAdapter } from 'nextsheet'
import { superSheetAdapter } from 'nextsheet/adapters/supersheet'

| Adapter | Output | Status | |---|---|---| | csvAdapter | .csv string | ✅ | | xlsxAdapter | .xlsx Buffer (via ExcelJS) | ✅ | | superSheetAdapter | SuperSheet JSON | ✅ | | Google Sheets | live sheet | ⏳ v0.3 |

Custom adapters

Implement the Adapter interface to target any output:

import type { Adapter, WorkbookNode, RenderResult } from 'nextsheet'

export const myAdapter: Adapter = {
  name: 'my-adapter',

  async render(workbook: WorkbookNode): Promise<RenderResult> {
    for (const sheet of workbook.sheets) {
      for (const section of sheet.sections) {
        for (const row of section.rows) {
          for (const cell of row.cells) {
            // cell.value, cell.format, cell.bold, cell.color …
          }
        }
      }
    }
    return { mimeType: 'text/plain', extension: 'txt', data: '…' }
  },
}

Cell formatting

<Cell format="currency">1500</Cell>   // $1,500.00
<Cell format="percent">0.12</Cell>    // 12.00%
<Cell format="number">1234567</Cell>  // 1,234,567
<Cell bold>Label</Cell>
<Cell color="green">+12%</Cell>       // red · green · blue · yellow · orange · purple · gray · #hex
<Cell colspan={3}>Merged</Cell>

IR types

NextSheet compiles JSX to a plain IR (intermediate representation) — no virtual DOM, no reconciler.

WorkbookNode
  └── SheetNode[]
        ├── columns:  ColumnNode[]
        ├── header?:  HeaderNode
        ├── sections: SectionNode[]
        │     └── rows: RowNode[]
        │           └── cells: CellNode[]
        ├── rows:     RowNode[]
        └── charts:   ChartNode[]
              └── series: ChartSeriesNode[]

All types are exported:

import type {
  WorkbookNode, SheetNode, SectionNode, RowNode, CellNode,
  ColumnNode, HeaderNode, FormulaNode, SheetDefinition, Adapter, RenderResult,
  ChartNode, ChartSeriesNode, ChartPoint, ChartType,
} from 'nextsheet'

How it works

NextSheet ships a custom JSX runtime (jsxImportSource: "nextsheet"). When TypeScript compiles your .sheet.tsx, each JSX element calls jsx(Component, props) which immediately invokes the component function. There is no virtual DOM. Components are pure functions that return IR nodes. The adapter consumes the IR.

This means NextSheet has zero runtime overhead — there is nothing to mount, diff, or reconcile. A sheet is just a function call.


License

MIT — nextsheet/nextsheet