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.
git clone https://github.com/majiayu000/claude-skill-registry
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"
skills/data/ag-grid-patterns/SKILL.mdAG-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
| Component | Purpose | Props |
|---|---|---|
| Container for title/controls | children |
| Grid title with optional badge | title, badge? |
| Settings action button | onClick |
| The actual AG-Grid | (none - uses context) |
| Footer status area | children |
| Visual corner accents | variant? |
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
| Preset | Row Height | Font Size | Padding |
|---|---|---|---|
| 20px | 10px | 4px |
| 24px | 11px | 6px |
| 32px | 13px | 8px |
| 40px | 14px | 12px |
Behavior Presets
| Preset | Selection | Sorting | Drag | Resize |
|---|---|---|---|---|
| none | true | false | false |
| single | true | true | true |
| multiple | true | true | true |
| none | false | false | false |
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
| 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
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
| Component | File | Purpose |
|---|---|---|
| Tmnl.DataGrid | | Compound component |
| DataGridContext | | Per-grid context |
| composeAgGridTheme | | Variant → theme |
| Flash system | | Cell highlighting |
| ValueCellRenderer | | Context-aware |
| tmnlDenseDark | | Canonical variant |
| V2 tldraw shape | | Modern integration |
| Hybrid drag | | 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