[TERMINAL · SKILLS]
> mounting /skills...
> indexing 295 manifests...
> linking agents: claude · codex · gemini · cursor
> ready.
[░░░░░░░░░░░░░░░░░░░░░░░░░░░░] 0%
Terminal.skills
Use Cases/Build a Virtual Scroll Table

Build a Virtual Scroll Table

Build a virtual scroll table with row virtualization, column pinning, server-side sorting and filtering, cell editing, row selection, and export for rendering large datasets efficiently.

#redis#caching#database#pub-sub#queues
Works with:claude-codeopenai-codexgemini-clicursor

Skills stack · 5 skills

Avg quality 93/100·All SAFE
>

typescript

v

Not yet scored
View skill
>

redis

v1.0.0

Build applications with Redis — caching, session storage, pub/sub, streams, rate limiting, leaderboards, and queues. Use when tasks involve in-memory data storage, real-time messaging, distributed locking, or performance optimization with caching layers.

93/100 quality
1.81× impact
SAFE
View skill
>

postgresql

v1.0.0

Assists with designing schemas, writing performant queries, managing indexes, and operating PostgreSQL databases. Use when working with JSONB, full-text search, window functions, CTEs, row-level security, replication, or performance tuning. Trigger words: postgresql, postgres, sql, database, jsonb, rls, window functions, cte.

87/100 quality
1.53× impact
SAFE
View skill
>

hono

v1.0.0

You are an expert in Hono, the ultrafast web framework for the edge. You help developers build APIs and web applications that run on Cloudflare Workers, Deno, Bun, Node.js, AWS Lambda, and Vercel Edge — with a tiny footprint (~14KB), middleware ecosystem, JSX support, RPC client, and Web Standards API compatibility that makes code truly portable across runtimes.

93/100 quality
3.00× impact
SAFE
View skill
>

zod

v1.0.0

You are an expert in Zod, the TypeScript-first schema declaration and validation library. You help developers define schemas that validate data at runtime AND infer TypeScript types at compile time — eliminating the need to write types and validators separately. Used for API input validation, form validation, environment variables, config files, and any data boundary.

100/100 quality
1.21× impact
SAFE
View skill
$

The Problem

Oscar leads frontend at a 20-person analytics company. Their data tables show up to 100,000 rows of financial data. Rendering all rows in the DOM crashes the browser tab (2GB memory, 5-second freeze). They paginate to 50 rows/page but users need to see trends across thousands of rows. Sorting and filtering requires a full page reload. Column pinning doesn't exist — users lose context scrolling horizontally across 20 columns. They need virtual scrolling: render only visible rows, smooth 60fps scrolling through 100K rows, server-side sort/filter, pinned columns, and inline editing.

Step 1: Build the Virtual Table Engine

typescript
import { pool } from "../db";
import { Redis } from "ioredis";
const redis = new Redis(process.env.REDIS_URL!);

interface Column { id: string; label: string; width: number; type: "text" | "number" | "date" | "boolean"; sortable: boolean; filterable: boolean; pinned?: "left" | "right"; editable: boolean; }
interface TableState { columns: Column[]; sortBy: string | null; sortDirection: "asc" | "desc"; filters: Record<string, any>; selectedRows: Set<string>; scrollTop: number; viewportHeight: number; rowHeight: number; }
interface VirtualWindow { startIndex: number; endIndex: number; rows: any[]; totalRows: number; offsetTop: number; totalHeight: number; }

const ROW_HEIGHT = 40;
const OVERSCAN = 10;

export async function getVirtualWindow(table: string, state: TableState): Promise<VirtualWindow> {
  const visibleCount = Math.ceil(state.viewportHeight / ROW_HEIGHT);
  const startIndex = Math.max(0, Math.floor(state.scrollTop / ROW_HEIGHT) - OVERSCAN);
  const endIndex = startIndex + visibleCount + OVERSCAN * 2;

  let sql = `SELECT * FROM ${table}`;
  const params: any[] = [];
  let idx = 1;
  const whereClauses: string[] = [];

  for (const [col, value] of Object.entries(state.filters)) {
    if (typeof value === "string" && value) { whereClauses.push(`${col} ILIKE $${idx}`); params.push(`%${value}%`); idx++; }
    else if (typeof value === "object" && value.min !== undefined) { whereClauses.push(`${col} BETWEEN $${idx} AND $${idx + 1}`); params.push(value.min, value.max); idx += 2; }
  }
  if (whereClauses.length > 0) sql += ` WHERE ${whereClauses.join(" AND ")}`;
  if (state.sortBy) sql += ` ORDER BY ${state.sortBy} ${state.sortDirection}`;
  sql += ` LIMIT $${idx} OFFSET $${idx + 1}`;
  params.push(endIndex - startIndex, startIndex);

  const { rows } = await pool.query(sql, params);
  const { rows: [{ count: totalRows }] } = await pool.query(
    `SELECT COUNT(*) as count FROM ${table}${whereClauses.length > 0 ? " WHERE " + whereClauses.join(" AND ") : ""}`,
    params.slice(0, whereClauses.length)
  );

  return {
    startIndex, endIndex: startIndex + rows.length,
    rows, totalRows: parseInt(totalRows),
    offsetTop: startIndex * ROW_HEIGHT,
    totalHeight: parseInt(totalRows) * ROW_HEIGHT,
  };
}

export async function updateCell(table: string, rowId: string, column: string, value: any): Promise<void> {
  await pool.query(`UPDATE ${table} SET ${column} = $2 WHERE id = $1`, [rowId, value]);
}

export async function exportData(table: string, state: TableState, format: "csv" | "json"): Promise<string> {
  let sql = `SELECT * FROM ${table}`;
  const params: any[] = [];
  if (state.sortBy) sql += ` ORDER BY ${state.sortBy} ${state.sortDirection}`;
  const { rows } = await pool.query(sql, params);

  if (format === "json") return JSON.stringify(rows, null, 2);
  const headers = Object.keys(rows[0] || {}).join(",");
  const csvRows = rows.map((r: any) => Object.values(r).map((v) => `"${String(v).replace(/"/g, '""')}"`).join(","));
  return [headers, ...csvRows].join("\n");
}

export async function getColumnStats(table: string, column: string): Promise<{ min: any; max: any; distinct: number; nullCount: number }> {
  const { rows: [stats] } = await pool.query(
    `SELECT MIN(${column}) as min, MAX(${column}) as max, COUNT(DISTINCT ${column}) as distinct_count, COUNT(*) FILTER (WHERE ${column} IS NULL) as null_count FROM ${table}`
  );
  return { min: stats.min, max: stats.max, distinct: parseInt(stats.distinct_count), nullCount: parseInt(stats.null_count) };
}

Results

  • 100K rows rendered smoothly — only 30-40 DOM rows at any time; 60fps scrolling; browser memory: 2GB → 50MB
  • Server-side sort/filter — sorting 100K rows happens in PostgreSQL (<100ms); no client-side sort freeze; ILIKE filter searches in real-time
  • Inline editing — double-click cell to edit; changes saved to DB instantly; no separate edit form; spreadsheet-like UX
  • Column pinning — ID and Name columns pinned left; scroll horizontally through 20 columns without losing context; totals pinned right
  • CSV/JSON export — one-click export of filtered/sorted data; finance team gets exactly the view they filtered to; no full dataset dump