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

@c-time/simple-ex-grid

v1.4.0

Published

Excel-like grid component for React with clipboard, touch support, and cell selection

Readme

@c-time/simple-ex-grid

Excel-like grid component for React. Touch-first design with clipboard support, drag selection, and three operating modes.

Install

npm install @c-time/simple-ex-grid
# or
pnpm add @c-time/simple-ex-grid

Peer dependencies: React 19+

Quick Start

import { SimpleExGrid } from '@c-time/simple-ex-grid'
import type { ColumnDef } from '@c-time/simple-ex-grid'

interface Row {
  id: number
  name: string
  price: number
}

const columns: ColumnDef<Row>[] = [
  { key: 'id', header: 'ID', width: 60, editable: false },
  { key: 'name', header: 'Name', width: 200 },
  { key: 'price', header: 'Price', width: 120, parser: (s) => Number(s) || 0 },
]

function App() {
  const [rows, setRows] = useState<Row[]>([
    { id: 1, name: 'Item A', price: 100 },
    { id: 2, name: 'Item B', price: 200 },
  ])

  return (
    <SimpleExGrid
      rows={rows}
      columns={columns}
      mode="edit"
      onChange={setRows}
    />
  )
}

Modes

| Mode | Description | |------|-------------| | select (default) | Selection and copy/paste only. No cell editing. | | edit | Re-tap active cell or press Enter/F2 to start editing. | | readonly | Selection and copy only. All mutations disabled. |

Props

Data

| Prop | Type | Default | Description | |------|------|---------|-------------| | rows | Row[] | required | Row data array | | columns | ColumnDef<Row>[] | required | Column definitions | | onChange | (rows, changes) => void | - | Called when cell values change |

Mode & Display

| Prop | Type | Default | Description | |------|------|---------|-------------| | mode | 'select' \| 'edit' \| 'readonly' | 'select' | Operating mode | | showRowGripHeader | boolean | true | Show row grip headers (left) | | showColumnGripHeader | boolean | false | Show column grip headers (top) | | rowHeight | 'single-line' \| 'fit-content' | 'single-line' | single-line: truncate with ellipsis. fit-content: wrap text |

Column Definition

| Property | Type | Default | Description | |----------|------|---------|-------------| | key | keyof Row & string | required | Row property key | | header | string | required | Header display text | | width | number \| '${n}%' \| 'fit-content' | 120 | Column width. Fixed px, percentage, or auto | | editable | boolean | true | Set false to make column read-only | | formatter | (value, row) => string | - | Display formatter | | parser | (input) => unknown | - | Input parser (string to value) | | validator | (value) => string \| null | - | Validation (return error message or null) | | sortMark | 'asc' \| 'desc' \| null | null | Display sort indicator (▲/▼) in header. Display only — sorting logic is the caller's responsibility. | | align | 'left' \| 'center' \| 'right' | - | Default text alignment for the column | | headerAlign | 'left' \| 'center' \| 'right' | - | Header text alignment (overrides align) | | color | string | - | Default text color for the column (any CSS color) | | fontStyle | 'normal' \| 'italic' | - | Default font style for the column | | fontWeight | 'normal' \| 'bold' | - | Default font weight for the column | | hidden | boolean | false | Hide column visually. Hidden columns are still included in clipboard operations. | | render | (value, row, rowIndex) => ReactNode | - | Custom cell renderer. Returned node replaces default text display. |

Pagination

| Prop | Type | Default | Description | |------|------|---------|-------------| | pagination | boolean | false | Enable pagination UI | | pageSize | number | 20 | Rows per page | | page | number | - | Controlled current page (0-indexed) | | onPageChange | (page) => void | - | Page change callback | | onPageSizeChange | (pageSize) => void | - | Page size change callback |

Selection

| Prop | Type | Default | Description | |------|------|---------|-------------| | selection | CellRange \| null | - | Controlled selection range | | onSelectionChange | (range) => void | - | Selection change callback | | onActiveCellChange | (row, col) => void | - | Active cell move callback |

Clipboard

| Prop | Type | Default | Description | |------|------|---------|-------------| | clipboard.enabled | boolean | true | Enable clipboard operations | | clipboard.format | 'tsv' \| 'html' \| 'both' | 'both' | Clipboard data format |

Keyboard shortcuts: Ctrl+C copy, Ctrl+X cut, Ctrl+V insert paste (add rows), Ctrl+Shift+V overwrite paste.

Cell Layout

| Prop | Type | Default | Description | |------|------|---------|-------------| | colSpan | (row, col) => number | - | Return column span for a cell. Spanned cells are merged visually. | | cellAlign | (row, col) => 'left' \| 'center' \| 'right' \| undefined | - | Per-cell text alignment override (takes priority over column align). | | cellColor | (row, col) => string \| undefined | - | Per-cell text color override (takes priority over column color). | | cellFontStyle | (row, col) => 'normal' \| 'italic' \| undefined | - | Per-cell font style override (takes priority over column fontStyle). | | cellFontWeight | (row, col) => 'normal' \| 'bold' \| undefined | - | Per-cell font weight override (takes priority over column fontWeight). |

Cell Badge

| Prop | Type | Default | Description | |------|------|---------|-------------| | cellBadge | (row, col) => CellBadge \| undefined | - | Show a colored dot on the cell's top-right corner. Hover/tap to display a tooltip. |

CellBadge has two properties: type ('success' / 'warning' / 'error') and message (tooltip text).

| Type | Color | Use case | |------|-------|----------| | success | Green (#27ae60) | Approved, valid, complete | | warning | Orange (#f39c12) | Caution, approaching limit | | error | Red (#e74c3c) | Validation error, over limit |

<SimpleExGrid
  rows={rows}
  columns={columns}
  cellBadge={(row, col) => {
    if (col === 2 && rows[row].price >= 10000)
      return { type: 'success', message: '承認済み' }
    if (col === 3 && rows[row].qty >= 200)
      return { type: 'error', message: `上限超過: ${rows[row].qty}` }
  }}
/>

Badge values are display-only and not included in clipboard operations.

Row Mutation

| Prop | Type | Default | Description | |------|------|---------|-------------| | allowRowAdd | boolean | false | Allow adding rows (paste auto-extend, insert paste) | | allowRowDelete | boolean | false | Allow deleting rows (context menu) | | onRowsAdd | (startRow, count) => void | - | Fires after rows are added | | onRowsDelete | (startRow, count) => void | - | Fires after rows are deleted |

Undo / Redo

| Prop | Type | Default | Description | |------|------|---------|-------------| | maxHistory | number | - | Maximum number of history entries to keep | | shouldRecordHistory | (changes, rowEvent?) => boolean | - | Filter which mutations are recorded. rowEvent is { kind, startRow, count } for row add/delete. | | historyApiRef | React.MutableRefObject<HistoryApi<Row>> | - | Ref to access undo/redo API externally |

HistoryApi<Row> exposes undo(), redo(), canUndo, canRedo, and pushHistory(before, after).

Keyboard shortcuts: Ctrl+Z undo, Ctrl+Y / Ctrl+Shift+Z redo.

Context Menu

| Prop | Type | Description | |------|------|-------------| | contextMenuItems | (defaultItems, context) => ContextMenuItem[] | Customize right-click menu items. Receives built-in items and context info, returns the final menu items. |

ContextMenuContext provides cell ({ row, col } | null), selection (CellRange | null), and mode.

<SimpleExGrid
  rows={rows}
  columns={columns}
  contextMenuItems={(defaultItems, { cell }) => [
    ...defaultItems,
    { type: 'separator' },
    { label: '行を複製', disabled: !cell, onClick: () => duplicateRow(cell!.row) },
  ]}
/>

Editing Lifecycle

| Prop | Type | Description | |------|------|-------------| | onEditStart | (row, col) => boolean \| void | Fires when editing begins. Return false to cancel. | | onEditCommit | (row, col, before, after) => boolean \| void | Fires on edit confirm. Return false to prevent write. | | onEditCancel | (row, col) => void | Fires when editing is cancelled (Escape). |

Clipboard Lifecycle

| Prop | Type | Description | |------|------|-------------| | onBeforeCopy | (context: BeforeCopyContext) => string[][] \| false \| void | Fires before copy/cut. Return false to cancel, string[][] to replace data, or void to proceed. | | onBeforePaste | (context: BeforePasteContext) => string[][] \| false \| void | Fires before paste. Return false to cancel, string[][] to replace data, or void to proceed. |

BeforeCopyContext provides data (2D string array), source ({ row, col, endRow, endCol }), cut (boolean), and a mutable metadata (Record<string, unknown>) for attaching custom data to the clipboard.

BeforePasteContext provides data (2D string array), target ({ row, col }), insert (boolean — true for insert-paste, false for overwrite-paste), and metadata (ClipboardMeta | nullnull when pasted from an external source).

Clipboard Metadata

When copying from the grid, a ClipboardMeta object is automatically written to the clipboard alongside the TSV/HTML data using a custom MIME type (web application/x-seg-clipboard). This metadata is available in onBeforePaste when pasting back into the same or another grid instance.

ClipboardMeta contains:

  • source — the original copy range ({ row, col, endRow, endCol })
  • columnKeys — column keys of the copied range
  • colSpans — per-row colspan values for owner columns
  • custom — user-defined metadata set via onBeforeCopy's context.metadata
<SimpleExGrid
  rows={rows}
  columns={columns}
  onBeforeCopy={(ctx) => {
    // Attach custom metadata to the clipboard
    ctx.metadata.sourceSheet = 'Sheet1'
    ctx.metadata.copiedAt = Date.now()
  }}
  onBeforePaste={(ctx) => {
    if (ctx.metadata) {
      console.log('Pasted from grid:', ctx.metadata.custom.sourceSheet)
      console.log('Column keys:', ctx.metadata.columnKeys)
    } else {
      console.log('Pasted from external source')
    }
  }}
/>

Features

Selection

  • Click to select, Shift+Click to extend range, drag for rectangle selection
  • Arrow keys to navigate, Tab to move right (wraps), Shift+Tab to move left
  • Ctrl+A to select all, Escape to clear selection
  • Grip headers: click to select entire row/column, drag to extend

Editing (edit mode)

  • Re-tap active cell, press Enter, F2, or type a character to begin editing
  • Enter commits and moves down, Tab commits and moves right, Escape cancels
  • Double-click also starts editing in any writable mode

Clipboard

  • Copy/cut/paste with Excel-compatible TSV + HTML table format
  • Paste auto-extends rows when allowRowAdd is enabled
  • Insert paste (Ctrl+V) inserts new rows at selection position
  • Overwrite paste (Ctrl+Shift+V) overwrites existing cells
  • Ctrl+I insert rows, Ctrl+D delete selected rows
  • Right-click context menu for all clipboard and row operations
  • Undo/redo (Ctrl+Z / Ctrl+Y) tracks all cell edits, paste, and row operations

Touch Support

  • Pointer Events API for unified mouse/touch/pen handling
  • setPointerCapture for reliable touch drag selection
  • Long-press opens context menu without losing selection (deferred selection pattern)

Layout

  • Sticky grip headers (row headers stick left, column headers stick top)
  • Custom overlay scrollbars (no layout shift from native scrollbar appearance)
  • Works in fixed-size containers, percentage-based layouts, and unconstrained layouts
  • Flex-based scroll structure handles both constrained and auto-sized parents

Development

pnpm install
pnpm dev          # Storybook dev server
pnpm test         # Vitest
pnpm typecheck    # TypeScript
pnpm lint         # ESLint
pnpm build        # Vite library build

License

MIT