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.
Best use case
ag-grid-patterns is best used when you need a repeatable AI agent workflow instead of a one-off prompt.
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.
Teams using ag-grid-patterns should expect a more consistent output, faster repeated execution, less prompt rewriting.
When to use this skill
- You want a reusable workflow that can be run more than once with consistent structure.
When not to use this skill
- You only need a quick one-off answer and do not need a reusable workflow.
- You cannot install or maintain the underlying files, dependencies, or repository context.
Installation
Claude Code / Cursor / Codex
Manual Installation
- Download SKILL.md from GitHub
- Place it in
.claude/skills/ag-grid-patterns/SKILL.mdinside your project - Restart your AI agent — it will auto-discover the skill
How ag-grid-patterns Compares
| Feature / Agent | ag-grid-patterns | Standard Approach |
|---|---|---|
| Platform Support | Not specified | Limited / Varies |
| Context Awareness | High | Baseline |
| Installation Complexity | Unknown | N/A |
Frequently Asked Questions
What does this skill do?
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.
Where can I find the source code?
You can find the source code on GitHub using the link provided at the top of the page.
SKILL.md Source
# AG-Grid Patterns for TMNL
## Critical: AG-Grid v34 Module Registration
**Without this, grid renders blank. No exceptions.**
```typescript
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.
```tsx
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
| Component | Purpose | Props |
|-----------|---------|-------|
| `Header` | Container for title/controls | children |
| `Title` | Grid title with optional badge | title, badge? |
| `SettingsButton` | Settings action button | onClick |
| `Body` | The actual AG-Grid | (none - uses context) |
| `StatusBar` | Footer status area | children |
| `CornerDecorations` | Visual corner accents | variant? |
### Per-Grid Runtime Isolation
Each `<Tmnl.DataGrid>` creates its own Effect runtime with isolated services:
```typescript
// 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
```typescript
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
| Preset | Row Height | Font Size | Padding |
|--------|------------|-----------|---------|
| `dense` | 20px | 10px | 4px |
| `compact` | 24px | 11px | 6px |
| `normal` | 32px | 13px | 8px |
| `comfortable` | 40px | 14px | 12px |
### Behavior Presets
| Preset | Selection | Sorting | Drag | Resize |
|--------|-----------|---------|------|--------|
| `readonly` | none | true | false | false |
| `interactive` | single | true | true | true |
| `editable` | multiple | true | true | true |
| `minimal` | none | false | false | false |
### Variant → Theme Conversion
```typescript
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
| Delta | Severity | Visual Effect |
|-------|----------|---------------|
| 0 | none | No flash |
| 1-5 | low | Subtle background |
| 6-10 | medium | Visible pulse |
| 11-15 | high | Strong glow |
| 16+ | critical | Full glow + pulse |
### Flash State Structure
```typescript
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
```typescript
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
```tsx
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
```typescript
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
```typescript
// Required context (throws if missing)
const ctx = useDataGridContext()
// Optional context (returns null if outside grid)
const ctx = useDataGridContextMaybe()
```
### Color Extraction from Context
```typescript
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
```typescript
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
```typescript
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
```typescript
interface DragState {
isDragging: boolean
isOutsideGrid: boolean // Key flag for phase transition
rowData: DataGridRow | null
ghostId: string | null // tldraw shape ID
}
```
### Phase Handlers
```typescript
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
```typescript
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.
```typescript
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.
```typescript
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)
```typescript
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)
```typescript
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
```typescript
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
```typescript
// 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
```typescript
// BANNED - Grid will render blank
<AgGridReact rowData={data} columnDefs={cols} />
// Must register modules FIRST
```
### 3. useState for Grid Data
```typescript
// BANNED when data crosses boundaries
const [rowData, setRowData] = useState([])
// Use Atom.make + service methods instead
```
### 4. Direct Theme Customization
```typescript
// BANNED - Bypasses variant system
const theme = themeQuartz.withParams({ backgroundColor: '#123' })
// Use GridVariant + composeAgGridTheme() instead
```
### 5. Cell Renderer Without Context Fallback
```typescript
// 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
```typescript
// 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
| Component | File | Purpose |
|-----------|------|---------|
| **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 GridDragServiceRelated Skills
ai-sdk-patterns
Vercel AI SDK tool patterns for dx-toolkit - input schemas for smart queries, API key handling, raw response returns
ai-cache-patterns
Embedding/vector caching for AI cost optimization
ai-anti-patterns
This skill should be used when reviewing AI-generated text, checking for AI writing patterns, detecting undisclosed AI content, or before finalizing any written content. Covers 12 categories of AI writing indicators from Wikipedia's comprehensive guide.
agency-workflow-patterns
Master orchestration patterns, multi-agent coordination, and effective workflow composition using the Agency plugin's 51+ specialized agents. Activate when planning complex implementations, coordinating multiple agents, or optimizing development workflows.
advanced-typescript-patterns
Advanced TypeScript patterns for TMNL. Covers conditional types, mapped types, branded types, generic constraints, type inference, and utility type composition. Pure TypeScript patterns beyond Effect Schema.
Advanced GetX Patterns
Advanced GetX features including Workers, GetxService, SmartManagement, GetConnect, GetSocket, bindings composition, and testing patterns
ActiveRecord Query Patterns
Complete guide to ActiveRecord query optimization, associations, scopes, and PostgreSQL-specific patterns. Use this skill when writing database queries, designing model associations, creating migrations, optimizing query performance, or debugging N+1 queries and grouping errors.
Action Cable & WebSocket Patterns
Real-time WebSocket features with Action Cable in Rails. Use when: (1) Building real-time chat, (2) Live notifications/presence, (3) Broadcasting model updates, (4) WebSocket authorization. Trigger keywords: Action Cable, WebSocket, real-time, channels, broadcasting, stream, subscriptions, presence, cable
accessibility-patterns
Build inclusive web experiences following WCAG guidelines. Covers semantic HTML, ARIA, keyboard navigation, color contrast, and testing strategies. Triggers on accessibility, a11y, WCAG, screen readers, or inclusive design requests.
abp-service-patterns
ABP Framework application layer patterns including AppServices, DTOs, Mapperly mapping, Unit of Work, and common patterns like Filter DTOs and ResponseModel. Use when: (1) creating AppServices, (2) mapping DTOs with Mapperly, (3) implementing list filtering, (4) wrapping API responses.
abp-infrastructure-patterns
ABP Framework cross-cutting patterns including authorization, background jobs, distributed events, multi-tenancy, and module configuration. Use when: (1) defining permissions, (2) creating background jobs, (3) publishing/handling distributed events, (4) configuring modules.
abp-entity-patterns
ABP Framework domain layer patterns including entities, aggregates, repositories, domain services, and data seeding. Use when: (1) creating entities with proper base classes, (2) implementing custom repositories, (3) writing domain services, (4) seeding data.