/** * useRuntimeHeightAugment * * For columns with custom cellRenderer that return {vnode, minHeight, minWidth}, * we need to measure actual DOM height after render and maintain a running * average per column to self-adjust row heights. */ import { ref, reactive, type Ref } from "vue"; const SAMPLE_THRESHOLD = 5; // Minimum samples before updating average const RECOMPUTE_THRESHOLD = 0.1; // 10% shift triggers recompute export interface HeightSample { columnKey: string; height: number; timestamp: number; } export interface ColumnHeightStats { columnKey: string; samples: number[]; average: number; count: number; } export function useRuntimeHeightAugment() { // Map from columnKey -> stats const columnStats = reactive>(new Map()); // Pending samples not yet incorporated const pendingSamples = ref([]); /** * Record a measured height for a specific column's cell */ function recordHeight(columnKey: string, height: number) { pendingSamples.value.push({ columnKey, height, timestamp: Date.now(), }); } /** * Flush pending samples and update running averages * Returns true if any column's average changed significantly */ function flushSamples(): boolean { if (pendingSamples.value.length === 0) return false; const changed: string[] = []; // Group samples by column const grouped = new Map(); for (const sample of pendingSamples.value) { const existing = grouped.get(sample.columnKey) || []; existing.push(sample.height); grouped.set(sample.columnKey, existing); } // Update stats for each column for (const [columnKey, heights] of grouped) { let stats = columnStats.get(columnKey); if (!stats) { stats = { columnKey, samples: [], average: 0, count: 0, }; columnStats.set(columnKey, stats); } // Add new samples stats.samples.push(...heights); stats.count += heights.length; // Keep only last 20 samples per column for running average if (stats.samples.length > 20) { stats.samples = stats.samples.slice(-20); } // Recompute average const newAverage = stats.samples.reduce((sum, h) => sum + h, 0) / stats.samples.length; // Check if change is significant if (stats.count >= SAMPLE_THRESHOLD) { const oldAverage = stats.average; if (oldAverage > 0 && Math.abs(newAverage - oldAverage) / oldAverage > RECOMPUTE_THRESHOLD) { changed.push(columnKey); } } stats.average = newAverage; } // Clear pending pendingSamples.value = []; return changed.length > 0; } /** * Get the current estimated height for a column */ function getColumnHeight(columnKey: string): number { const stats = columnStats.get(columnKey); return stats?.average ?? 44; // Default fallback } /** * Get all column heights as a map */ function getAllColumnHeights(): Map { const result = new Map(); for (const [key, stats] of columnStats) { result.set(key, stats.average || 44); } return result; } /** * Reset stats for a column */ function resetColumn(columnKey: string) { columnStats.delete(columnKey); } /** * Reset all stats */ function resetAll() { columnStats.clear(); pendingSamples.value = []; } return { columnStats, pendingSamples, recordHeight, flushSamples, getColumnHeight, getAllColumnHeights, resetColumn, resetAll, }; }