Claude-skill-registry ag-grid-patterns

AG-Grid v34 integration patterns for TMNL. Invoke when implementing data grids, custom cell renderers, themes, or grid-based UI. Provides canonical file locations and pattern precedents.

install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/ag-grid-patterns" ~/.claude/skills/majiayu000-claude-skill-registry-ag-grid-patterns && rm -rf "$T"
manifest: skills/data/ag-grid-patterns/SKILL.md
source content

AG-Grid Patterns for TMNL

Critical: AG-Grid v34 Module Registration

Without this, grid renders blank. No exceptions.

import { ModuleRegistry, AllCommunityModule } from 'ag-grid-community'
ModuleRegistry.registerModules([AllCommunityModule])

No CSS imports needed when using the

theme
prop.


Canonical Sources

Core Library (v2)

  • Compound component:
    src/lib/data-grid/components/UnifiedDataGrid.tsx
  • Context system:
    src/lib/data-grid/components/DataGridContext.tsx
  • Theme composer:
    src/lib/data-grid/composer/theme-composer.ts
  • Flash system:
    src/lib/data-grid/flash/index.ts
  • Cell renderers:
    src/lib/data-grid/renderers/
  • Variants:
    src/lib/data-grid/variants/
  • Barrel export:
    src/lib/data-grid/index.ts

tldraw Integration

  • V2 shape (modern):
    src/components/tldraw/shapes/data-grid-shape-v2.tsx
  • V1 shape (legacy):
    src/components/tldraw/shapes/data-grid-shape.tsx
  • Hybrid drag:
    src/components/tldraw/shapes/data-grid-shape.tsx:366-594

Architecture Documentation

  • Deep dive:
    assets/documents/AG_GRID_THEMING_ARCHITECTURE.md

Pattern 1: Tmnl.DataGrid — COMPOUND COMPONENT API

When: Building any data grid in TMNL.

The modern API uses compound components for declarative composition.

import { Tmnl } from '@/lib/data-grid'
import { tmnlDenseDark } from '@/lib/data-grid/variants/tmnl-dense-dark'

<Tmnl.DataGrid
  id="emitters"
  variant={tmnlDenseDark}
  rowData={data}
  columnDefs={columnDefs}
>
  <Tmnl.DataGrid.Header>
    <Tmnl.DataGrid.Title title="EMITTERS" badge={data.length} />
    <Tmnl.DataGrid.SettingsButton onClick={openSettings} />
  </Tmnl.DataGrid.Header>

  <Tmnl.DataGrid.Body />

  <Tmnl.DataGrid.StatusBar>
    <span className="text-cyan-500">V2</span>
    <span>{data.length} rows</span>
  </Tmnl.DataGrid.StatusBar>

  <Tmnl.DataGrid.CornerDecorations />
</Tmnl.DataGrid>

Child Components

ComponentPurposeProps
Header
Container for title/controlschildren
Title
Grid title with optional badgetitle, badge?
SettingsButton
Settings action buttononClick
Body
The actual AG-Grid(none - uses context)
StatusBar
Footer status areachildren
CornerDecorations
Visual corner accentsvariant?

Per-Grid Runtime Isolation

Each

<Tmnl.DataGrid>
creates its own Effect runtime with isolated services:

// Inside Tmnl.DataGrid provider
const runtime = useMemo(() => createDataGridRuntime(), [gridId])

// Child components access via context
const ctx = useDataGridContext()
const variant = ctx.variant      // Reactive variant
const gridApi = ctx.gridApi      // Reactive grid API

Canonical source:

src/lib/data-grid/components/UnifiedDataGrid.tsx


Pattern 2: GridVariant System — VARIANT-DRIVEN THEMING

When: Customizing grid appearance, density, or behavior.

Variants are not just themes. They encode:

  • Density — Row height, font size, padding
  • Colors — Background, text, signals, flash
  • Behavior — Selection, sorting, drag, resize
  • Typography — Font family, letter spacing, weight

Variant Structure

export const tmnlDenseDark: GridVariant = {
  id: 'tmnl-dense-dark',
  densityTier: 'dense',
  density: DENSITY_PRESETS.dense,
  colorScheme: 'dark',

  colors: {
    background: { base: '#000000', alternateRow: '#0a0a0a', header: '#0d0d0d' },
    text: { primary: '#ffffff', secondary: '#a3a3a3', muted: '#525252' },
    signal: { positive: '#22c55e', negative: '#ef4444', accent: '#00ffcc' },
    border: { primary: '#262626', muted: '#1a1a1a' },
    flash: {
      up: 'rgba(34, 197, 94, 0.4)',
      down: 'rgba(239, 68, 68, 0.4)',
      durationMs: 1500,
    },
  },

  behavior: BEHAVIOR_PRESETS.interactive,

  typography: {
    fontFamily: "'JetBrains Mono', monospace",
    headerLetterSpacing: '0.05em',
  },

  intentOverrides: {
    // Per-column styling rules
  },
}

Density Presets

PresetRow HeightFont SizePadding
dense
20px10px4px
compact
24px11px6px
normal
32px13px8px
comfortable
40px14px12px

Behavior Presets

PresetSelectionSortingDragResize
readonly
nonetruefalsefalse
interactive
singletruetruetrue
editable
multipletruetruetrue
minimal
nonefalsefalsefalse

Variant → Theme Conversion

import { composeAgGridTheme } from '@/lib/data-grid/composer/theme-composer'

const agTheme = composeAgGridTheme(tmnlDenseDark)
// Returns AG-Grid themeQuartz.withParams({...})

Canonical source:

src/lib/data-grid/variants/tmnl-dense-dark.ts


Pattern 3: Flash System — CELL CHANGE HIGHLIGHTING

When: Showing real-time data updates with visual feedback.

Severity Mapping

DeltaSeverityVisual Effect
0noneNo flash
1-5lowSubtle background
6-10mediumVisible pulse
11-15highStrong glow
16+criticalFull glow + pulse

Flash State Structure

interface FlashState {
  severity: 'none' | 'low' | 'medium' | 'high' | 'critical'
  intensity: number      // 0-1, logarithmic scale
  direction: 'up' | 'down' | 'neutral'
  delta: number
  timestamp: number
  isActive: boolean
}

useFlashTracker Hook

import { useFlashTracker } from '@/lib/data-grid/flash'

const { getFlashState, hasFlash, processUpdates, injectKeyframes } = useFlashTracker({
  maxDelta: 20,
  flashExpirationMs: 1500,
})

// Initialize (once)
useEffect(() => injectKeyframes(), [])

// Process row updates
useEffect(() => {
  processUpdates(newData, oldData, 'value')
}, [newData])

// In cell renderer
const flash = getFlashState(rowId, field)
const styles = generateFlashStyles(flash, { colors: variant.colors.flash })

Flash in Cell Renderer

function ValueCellRenderer(params: ICellRendererParams) {
  const ctx = useDataGridContext()
  const { getFlashState } = ctx.flash

  const flash = getFlashState(params.node.id, params.colDef.field)

  return (
    <div
      className={flash.isActive ? `flash-${flash.severity}` : ''}
      style={{
        backgroundColor: flash.isActive
          ? flash.direction === 'up'
            ? ctx.variant.colors.flash.up
            : ctx.variant.colors.flash.down
          : undefined,
      }}
    >
      {params.value}
    </div>
  )
}

Canonical source:

src/lib/data-grid/flash/index.ts


Pattern 4: DataGridContext — PER-GRID SERVICES

When: Child components need access to grid state, variant, or API.

Context Shape

interface DataGridContextValue<TData = unknown> {
  gridId: string
  runtime: DataGridRuntime           // Per-grid Effect services
  variant: GridVariantType           // Reactive variant
  rowData: TData[]
  columnDefs: ColDef<TData>[]
  getRowId?: GetRowIdFunc<TData>
  gridApi: GridApi | null            // AG-Grid API reference
  setGridApi: (api: GridApi | null) => void
  flash: FlashTrackerAPI
}

Usage in Components

// Required context (throws if missing)
const ctx = useDataGridContext()

// Optional context (returns null if outside grid)
const ctx = useDataGridContextMaybe()

Color Extraction from Context

function StatusCellRenderer(params: ICellRendererParams) {
  const ctx = useDataGridContextMaybe()

  // Variant colors with fallback
  const colors = ctx?.variant.colors ?? {
    signal: { positive: '#22c55e', negative: '#ef4444' }
  }

  return (
    <span style={{ color: colors.signal.positive }}>
      {params.value}
    </span>
  )
}

Canonical source:

src/lib/data-grid/components/DataGridContext.tsx


Pattern 5: Context-Aware Cell Renderers

When: Renderers need variant colors or grid services.

Pattern: Fallback for Standalone Usage

import { useDataGridContextMaybe } from '@/lib/data-grid'
import { COLORS } from '@/lib/tokens'

export function ValueCellRenderer(params: ValueCellRendererParams) {
  const ctx = useDataGridContextMaybe()

  // Colors from variant OR token fallback
  const textColor = ctx?.variant.colors.text.primary ?? COLORS.textPrimary
  const accentColor = ctx?.variant.colors.signal.accent ?? COLORS.accentCyan

  return (
    <div style={{ color: textColor }}>
      <span>{params.value}</span>
      <div
        style={{
          backgroundColor: accentColor,
          width: `${params.data.percentage}%`,
        }}
      />
    </div>
  )
}

Pattern: Flash-Aware Renderer

export function FlashValueRenderer(params: ICellRendererParams) {
  const ctx = useDataGridContext()
  const flash = ctx.flash.getFlashState(params.node.id, params.colDef.field!)

  return (
    <div
      className={cn(
        'transition-all duration-300',
        flash.isActive && `flash-${flash.severity}`,
        flash.direction === 'up' && 'text-green-400',
        flash.direction === 'down' && 'text-red-400',
      )}
    >
      {params.value}
    </div>
  )
}

Canonical source:

src/lib/data-grid/renderers/ValueCellRenderer.tsx


Pattern 6: Hybrid Drag System — GRID-TO-CANVAS

When: Dragging rows from AG-Grid onto a tldraw canvas.

Drag Phase Transitions

┌─────────────────────────────────┐
│   AG-Grid Internal Drag         │
│   (rowDragManaged=true)         │
└──────────────┬──────────────────┘
               │ onRowDragMove
               ▼
        Check: outside grid bounds?
               │
        ┌──────┴──────┐
       NO            YES
        │             │
     Continue    Create ghost
     in grid     shape on canvas
        │             │
        │      onPointerMove
        │      updateGhost()
        │             │
        └─────┬───────┘
              │ onPointerUp
        spawnDataCard()
        Remove ghost

Drag State Schema

interface DragState {
  isDragging: boolean
  isOutsideGrid: boolean    // Key flag for phase transition
  rowData: DataGridRow | null
  ghostId: string | null    // tldraw shape ID
}

Phase Handlers

const onRowDragMove = useCallback((event: RowDragMoveEvent) => {
  const { clientX, clientY } = event.event
  const gridRect = gridRef.current?.getBoundingClientRect()

  const isOutside = !gridRect ||
    clientX < gridRect.left || clientX > gridRect.right ||
    clientY < gridRect.top || clientY > gridRect.bottom

  if (isOutside && !dragState.isOutsideGrid) {
    // Transition: GridInternal → CanvasTracking
    const ghostId = createGhostShape(dragState.rowData, { x: clientX, y: clientY })
    setDragState(prev => ({ ...prev, isOutsideGrid: true, ghostId }))
  } else if (!isOutside && dragState.isOutsideGrid) {
    // Transition: CanvasTracking → GridInternal
    removeGhostShape(dragState.ghostId)
    setDragState(prev => ({ ...prev, isOutsideGrid: false, ghostId: null }))
  }
}, [dragState, createGhostShape, removeGhostShape])

Effect Service for Drag

export interface GridDragServiceApi {
  readonly getState: Effect.Effect<DragState>
  readonly dispatch: (event: GridDragEvent) => Effect.Effect<void>
  readonly subscribe: (handler: (state: DragState) => void) => Effect.Effect<() => void>
  readonly isDragging: Effect.Effect<boolean>
  readonly getPhase: Effect.Effect<DragPhase>
}

Canonical source:

src/lib/data-grid/services/GridDragService.ts


Pattern 7: Theme Composition via composeAgGridTheme

When: Converting a GridVariant to an AG-Grid theme.

import { themeQuartz } from 'ag-grid-community'

export function composeAgGridTheme(variant: GridVariant) {
  const { colors, density, typography } = variant

  return themeQuartz.withParams({
    // Core colors
    backgroundColor: colors.background.base,
    foregroundColor: colors.text.primary,
    accentColor: colors.signal.accent,

    // Header
    headerBackgroundColor: colors.background.header,
    headerTextColor: colors.text.secondary,

    // Rows
    oddRowBackgroundColor: colors.background.alternateRow,
    rowHoverColor: `${colors.background.base}cc`,
    selectedRowBackgroundColor: `${colors.signal.accent}15`,

    // Typography
    fontFamily: typography.fontFamily,
    fontSize: density.fontSize,
    headerFontSize: density.fontSizeXs,

    // Density-driven spacing
    rowHeight: density.rowHeight,
    headerHeight: density.headerHeight,
    cellHorizontalPaddingScale: density.paddingX / 8,

    // Borders
    borderColor: colors.border.primary,
    wrapperBorderRadius: 0,
  })
}

Canonical source:

src/lib/data-grid/composer/theme-composer.ts


Pattern 8: Color Extraction Helpers

When: Extracting semantic colors from variant for custom UI.

export function extractStatusColors(variant: GridVariant) {
  return {
    active: variant.colors.signal.positive,
    pending: variant.colors.signal.warning ?? variant.colors.signal.accent,
    inactive: variant.colors.signal.neutral ?? variant.colors.text.muted,
    error: variant.colors.signal.negative,
    default: variant.colors.text.muted,
  } as const
}

export function extractFlashConfig(variant: GridVariant) {
  const flash = variant.colors.flash
  return {
    enabled: variant.behavior.microInteractions?.enableCellFlash ?? true,
    upColor: flash.up,
    downColor: flash.down,
    durationMs: flash.durationMs,
  }
}

Pattern 9: Legacy Basic Patterns

Theme Creation via themeQuartz (Direct)

import { themeQuartz } from 'ag-grid-community'
import { TMNL_TOKENS } from './data-grid-theme'

export const tmnlDataGridTheme = themeQuartz.withParams({
  backgroundColor: TMNL_TOKENS.colors.background,
  foregroundColor: TMNL_TOKENS.colors.text.primary,
  borderColor: TMNL_TOKENS.colors.border,
  headerBackgroundColor: TMNL_TOKENS.colors.surface,
  // ... etc
})

Basic Cell Renderer (Non-Context)

const IdCellRenderer = (params: ICellRendererParams) => (
  <span
    style={{
      color: TMNL_TOKENS.colors.text.muted,
      fontSize: TMNL_TOKENS.typography.sizes.xs,
      fontFamily: TMNL_TOKENS.typography.fontFamily,
      letterSpacing: '0.05em',
    }}
  >
    {params.value}
  </span>
)

Column Definitions

const columnDefs: ColDef[] = [
  {
    field: 'id',
    headerName: 'ID',
    width: 80,
    cellRenderer: IdCellRenderer,
    sortable: true,
  },
  {
    field: 'value',
    headerName: 'Value',
    width: 120,
    cellRenderer: ValueCellRenderer,
    comparator: (a, b) => a - b,
  },
]

Anti-Patterns (BANNED)

1. Importing CSS Files

// BANNED - No CSS imports with v34
import 'ag-grid-community/styles/ag-grid.css'
import 'ag-grid-community/styles/ag-theme-quartz.css'
// Use theme prop instead

2. Missing Module Registration

// BANNED - Grid will render blank
<AgGridReact rowData={data} columnDefs={cols} />
// Must register modules FIRST

3. useState for Grid Data

// BANNED when data crosses boundaries
const [rowData, setRowData] = useState([])
// Use Atom.make + service methods instead

4. Direct Theme Customization

// BANNED - Bypasses variant system
const theme = themeQuartz.withParams({ backgroundColor: '#123' })
// Use GridVariant + composeAgGridTheme() instead

5. Cell Renderer Without Context Fallback

// BANNED - Breaks outside grid context
function BadRenderer(params) {
  const ctx = useDataGridContext()  // Throws if no context!
  return <span style={{ color: ctx.variant.colors.text.primary }}>...</span>
}

// CORRECT - Graceful fallback
function GoodRenderer(params) {
  const ctx = useDataGridContextMaybe()
  const color = ctx?.variant.colors.text.primary ?? COLORS.textPrimary
  return <span style={{ color }}>...</span>
}

6. Creating Atoms Inside Components

// BANNED - Recreates on every render
function BadGrid() {
  const rowDataAtom = Atom.make([])  // BAD!
  return <AgGridReact ... />
}

// CORRECT - Module-level atoms
const rowDataAtom = Atom.make<RowData[]>([])
function GoodGrid() {
  const rowData = useAtomValue(rowDataAtom)
  return <AgGridReact rowData={rowData} ... />
}

Decision Tree: Which API to Use

Building a data grid?
│
├─ Simple, one-off grid?
│  └─ Use: Direct AgGridReact with composeAgGridTheme()
│
├─ Grid with header, status bar, controls?
│  └─ Use: Tmnl.DataGrid compound component
│
├─ Grid in tldraw shape?
│  └─ Use: V2 pattern (data-grid-shape-v2.tsx)
│
├─ Need real-time flash updates?
│  └─ Use: useFlashTracker + FlashValueRenderer
│
└─ Need custom variant?
   └─ Clone tmnlDenseDark and modify

File Locations Summary

ComponentFilePurpose
Tmnl.DataGrid
src/lib/data-grid/components/UnifiedDataGrid.tsx
Compound component
DataGridContext
src/lib/data-grid/components/DataGridContext.tsx
Per-grid context
composeAgGridTheme
src/lib/data-grid/composer/theme-composer.ts
Variant → theme
Flash system
src/lib/data-grid/flash/index.ts
Cell highlighting
ValueCellRenderer
src/lib/data-grid/renderers/ValueCellRenderer.tsx
Context-aware
tmnlDenseDark
src/lib/data-grid/variants/tmnl-dense-dark.ts
Canonical variant
V2 tldraw shape
src/components/tldraw/shapes/data-grid-shape-v2.tsx
Modern integration
Hybrid drag
src/components/tldraw/shapes/data-grid-shape.tsx:366-594
Grid-to-canvas

Integration Points

  • effect-atom-integration — Atom-as-State for rowData
  • tmnl-design-tokens — Token fallbacks in renderers
  • common-conventions — Barrel exports, naming patterns
  • effect-patterns — Effect.Service for GridDragService