@toolbox-web/grid 1.19.0 → 1.19.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/all.js +97 -86
  2. package/all.js.map +1 -1
  3. package/index.js +367 -341
  4. package/index.js.map +1 -1
  5. package/lib/core/grid.d.ts.map +1 -1
  6. package/lib/core/internal/rows.d.ts.map +1 -1
  7. package/lib/core/internal/utils.d.ts +2 -2
  8. package/lib/core/internal/utils.d.ts.map +1 -1
  9. package/lib/core/plugin/types.d.ts +0 -2
  10. package/lib/core/plugin/types.d.ts.map +1 -1
  11. package/lib/core/types.d.ts +11 -0
  12. package/lib/core/types.d.ts.map +1 -1
  13. package/lib/plugins/clipboard/index.js.map +1 -1
  14. package/lib/plugins/column-virtualization/index.js.map +1 -1
  15. package/lib/plugins/context-menu/index.js.map +1 -1
  16. package/lib/plugins/editing/index.js.map +1 -1
  17. package/lib/plugins/export/index.js.map +1 -1
  18. package/lib/plugins/filtering/index.js.map +1 -1
  19. package/lib/plugins/grouping-columns/index.js.map +1 -1
  20. package/lib/plugins/grouping-rows/index.js.map +1 -1
  21. package/lib/plugins/master-detail/MasterDetailPlugin.d.ts.map +1 -1
  22. package/lib/plugins/master-detail/index.js +62 -55
  23. package/lib/plugins/master-detail/index.js.map +1 -1
  24. package/lib/plugins/multi-sort/index.js.map +1 -1
  25. package/lib/plugins/pinned-columns/index.js.map +1 -1
  26. package/lib/plugins/pinned-rows/index.js.map +1 -1
  27. package/lib/plugins/pivot/index.js.map +1 -1
  28. package/lib/plugins/print/index.js.map +1 -1
  29. package/lib/plugins/reorder/index.js.map +1 -1
  30. package/lib/plugins/responsive/index.js.map +1 -1
  31. package/lib/plugins/row-reorder/index.js.map +1 -1
  32. package/lib/plugins/selection/SelectionPlugin.d.ts +3 -0
  33. package/lib/plugins/selection/SelectionPlugin.d.ts.map +1 -1
  34. package/lib/plugins/selection/index.js +179 -175
  35. package/lib/plugins/selection/index.js.map +1 -1
  36. package/lib/plugins/selection/types.d.ts +26 -0
  37. package/lib/plugins/selection/types.d.ts.map +1 -1
  38. package/lib/plugins/server-side/index.js.map +1 -1
  39. package/lib/plugins/tree/index.js.map +1 -1
  40. package/lib/plugins/undo-redo/index.js.map +1 -1
  41. package/lib/plugins/visibility/index.js.map +1 -1
  42. package/package.json +1 -1
  43. package/umd/grid.all.umd.js +24 -24
  44. package/umd/grid.all.umd.js.map +1 -1
  45. package/umd/grid.umd.js +11 -11
  46. package/umd/grid.umd.js.map +1 -1
  47. package/umd/plugins/master-detail.umd.js +1 -1
  48. package/umd/plugins/master-detail.umd.js.map +1 -1
  49. package/umd/plugins/selection.umd.js +2 -2
  50. package/umd/plugins/selection.umd.js.map +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":["../../../../../../libs/grid/src/lib/plugins/filtering/filter-model.ts","../../../../../../libs/grid/src/lib/core/internal/virtualization.ts","../../../../../../libs/grid/src/lib/core/types.ts","../../../../../../libs/grid/src/lib/core/plugin/base-plugin.ts","../../../../../../libs/grid/src/lib/core/plugin/expander-column.ts","../../../../../../libs/grid/src/lib/plugins/filtering/FilteringPlugin.ts"],"sourcesContent":["/**\n * Filter Model Core Logic\n *\n * Pure functions for filtering operations.\n */\n\nimport type { FilterModel } from './types';\n\n/**\n * Sentinel value used in set-filter unique values to represent rows with\n * no value (null, undefined, empty array via filterValue extractor).\n * Exported so server-side implementations can use the same constant.\n */\nexport const BLANK_FILTER_VALUE = '(Blank)';\n\n/**\n * Convert a value to a comparable number.\n * Handles Date objects, numeric values, and date/ISO strings.\n */\nfunction toNumeric(value: unknown): number {\n if (value instanceof Date) return value.getTime();\n const n = Number(value);\n if (!isNaN(n)) return n;\n // Try parsing as a date string (ISO 8601, etc.)\n const d = new Date(value as string);\n return d.getTime(); // NaN if unparseable\n}\n\n/**\n * Check if a single row matches a filter condition.\n *\n * @param row - The row data object\n * @param filter - The filter to apply\n * @param caseSensitive - Whether text comparisons are case sensitive\n * @param filterValue - Optional extractor for complex cell values (arrays, objects)\n * @returns True if the row matches the filter\n */\nexport function matchesFilter(\n row: Record<string, unknown>,\n filter: FilterModel,\n caseSensitive = false,\n filterValue?: (value: unknown, row: Record<string, unknown>) => unknown | unknown[],\n): boolean {\n const rawValue = row[filter.field];\n\n // Handle blank/notBlank first - these work on null/undefined/empty\n if (filter.operator === 'blank') {\n return rawValue == null || rawValue === '';\n }\n if (filter.operator === 'notBlank') {\n return rawValue != null && rawValue !== '';\n }\n\n // When a filterValue extractor is present, use array-aware matching for set operators.\n // Each extracted value is checked individually against the filter set.\n if (filterValue && (filter.operator === 'notIn' || filter.operator === 'in')) {\n const extracted = filterValue(rawValue, row);\n const values = Array.isArray(extracted) ? extracted : extracted != null ? [extracted] : [];\n\n if (filter.operator === 'notIn') {\n // Row is hidden if ANY extracted value is in the excluded set.\n // Empty values array (null/empty cell) → controlled by BLANK_FILTER_VALUE sentinel.\n const excluded = filter.value;\n if (!Array.isArray(excluded)) return true;\n if (values.length === 0) return !excluded.includes(BLANK_FILTER_VALUE);\n return !values.some((v) => excluded.includes(v));\n }\n if (filter.operator === 'in') {\n // Row passes if ANY extracted value is in the included set.\n // Empty values array (null/empty cell) → controlled by BLANK_FILTER_VALUE sentinel.\n const included = filter.value;\n if (!Array.isArray(included)) return false;\n if (values.length === 0) return included.includes(BLANK_FILTER_VALUE);\n return values.some((v) => included.includes(v));\n }\n }\n\n // Set operators handle null explicitly: null is never \"in\" a set,\n // and null is never excluded by \"notIn\" (it's not a listed value).\n if (filter.operator === 'notIn') {\n if (rawValue == null) return true;\n return Array.isArray(filter.value) && !filter.value.includes(rawValue);\n }\n if (filter.operator === 'in') {\n return Array.isArray(filter.value) && filter.value.includes(rawValue);\n }\n\n // Null/undefined values don't match other filters\n if (rawValue == null) return false;\n\n // Prepare values for comparison\n const stringValue = String(rawValue);\n const compareValue = caseSensitive ? stringValue : stringValue.toLowerCase();\n const compareFilterValue = caseSensitive ? String(filter.value) : String(filter.value).toLowerCase();\n\n switch (filter.operator) {\n // Text operators\n case 'contains':\n return compareValue.includes(compareFilterValue);\n\n case 'notContains':\n return !compareValue.includes(compareFilterValue);\n\n case 'equals':\n return compareValue === compareFilterValue;\n\n case 'notEquals':\n return compareValue !== compareFilterValue;\n\n case 'startsWith':\n return compareValue.startsWith(compareFilterValue);\n\n case 'endsWith':\n return compareValue.endsWith(compareFilterValue);\n\n // Number/Date operators (use toNumeric for Date objects and date strings)\n case 'lessThan':\n return toNumeric(rawValue) < toNumeric(filter.value);\n\n case 'lessThanOrEqual':\n return toNumeric(rawValue) <= toNumeric(filter.value);\n\n case 'greaterThan':\n return toNumeric(rawValue) > toNumeric(filter.value);\n\n case 'greaterThanOrEqual':\n return toNumeric(rawValue) >= toNumeric(filter.value);\n\n case 'between':\n return toNumeric(rawValue) >= toNumeric(filter.value) && toNumeric(rawValue) <= toNumeric(filter.valueTo);\n\n default:\n return true;\n }\n}\n\n/**\n * Filter rows based on multiple filter conditions (AND logic).\n * All filters must match for a row to be included.\n *\n * @param rows - The rows to filter\n * @param filters - Array of filters to apply\n * @param caseSensitive - Whether text comparisons are case sensitive\n * @param filterValues - Optional map of field → value extractor for complex columns\n * @returns Filtered rows\n */\nexport function filterRows<T extends Record<string, unknown>>(\n rows: T[],\n filters: FilterModel[],\n caseSensitive = false,\n filterValues?: Map<string, (value: unknown, row: T) => unknown | unknown[]>,\n): T[] {\n if (!filters.length) return rows;\n return rows.filter((row) =>\n filters.every((f) =>\n matchesFilter(\n row,\n f,\n caseSensitive,\n filterValues?.get(f.field) as\n | ((value: unknown, row: Record<string, unknown>) => unknown | unknown[])\n | undefined,\n ),\n ),\n );\n}\n\n/**\n * Compute a cache key for a set of filters.\n * Used for memoization of filter results.\n *\n * @param filters - Array of filters\n * @returns Stable string key for the filter set\n */\nexport function computeFilterCacheKey(filters: FilterModel[]): string {\n return JSON.stringify(\n filters.map((f) => ({\n field: f.field,\n operator: f.operator,\n value: f.value,\n valueTo: f.valueTo,\n })),\n );\n}\n\n/**\n * Extract unique values from a field across all rows.\n * Useful for populating \"set\" filter dropdowns.\n *\n * When `filterValue` is provided, the extractor is called for each row's cell value.\n * If it returns an array, each element is added individually (flattened).\n * This enables complex-valued cells (e.g., arrays of objects) to expose\n * their individual filterable values.\n *\n * @param rows - The rows to extract values from\n * @param field - The field name\n * @param filterValue - Optional extractor for complex cell values\n * @returns Sorted array of unique non-null values\n */\nexport function getUniqueValues<T extends Record<string, unknown>>(\n rows: T[],\n field: string,\n filterValue?: (value: unknown, row: T) => unknown | unknown[],\n): unknown[] {\n const values = new Set<unknown>();\n let hasBlank = false;\n for (const row of rows) {\n const cellValue = row[field];\n if (filterValue) {\n const extracted = filterValue(cellValue, row);\n if (Array.isArray(extracted)) {\n if (extracted.length === 0) {\n hasBlank = true;\n }\n for (const v of extracted) {\n if (v != null) values.add(v);\n }\n } else if (extracted != null) {\n values.add(extracted);\n } else {\n hasBlank = true;\n }\n } else {\n if (cellValue != null) {\n values.add(cellValue);\n }\n }\n }\n // When a filterValue extractor is present and some rows have no values,\n // include a \"(Blank)\" sentinel so users can explicitly filter empty rows.\n if (filterValue && hasBlank) {\n values.add(BLANK_FILTER_VALUE);\n }\n return [...values].sort((a, b) => {\n // Handle mixed types gracefully\n if (typeof a === 'number' && typeof b === 'number') {\n return a - b;\n }\n return String(a).localeCompare(String(b));\n });\n}\n\n/**\n * Extract unique values for multiple fields in a single pass through the rows.\n * This is more efficient than calling `getUniqueValues` N times when\n * computing derived state for several set filters at once.\n *\n * @param rows - The rows to extract values from\n * @param fields - Array of { field, filterValue? } descriptors\n * @returns Map of field → sorted unique values (same contract as `getUniqueValues`)\n */\nexport function getUniqueValuesBatch<T extends Record<string, unknown>>(\n rows: T[],\n fields: { field: string; filterValue?: (value: unknown, row: T) => unknown | unknown[] }[],\n): Map<string, unknown[]> {\n // Per-field accumulators\n const acc = new Map<string, { values: Set<unknown>; hasBlank: boolean; hasExtractor: boolean }>();\n for (const { field, filterValue } of fields) {\n acc.set(field, { values: new Set(), hasBlank: false, hasExtractor: !!filterValue });\n }\n\n // Single pass through all rows\n for (const row of rows) {\n for (const { field, filterValue } of fields) {\n const entry = acc.get(field)!;\n const cellValue = row[field];\n if (filterValue) {\n const extracted = filterValue(cellValue, row);\n if (Array.isArray(extracted)) {\n if (extracted.length === 0) entry.hasBlank = true;\n for (const v of extracted) {\n if (v != null) entry.values.add(v);\n }\n } else if (extracted != null) {\n entry.values.add(extracted);\n } else {\n entry.hasBlank = true;\n }\n } else {\n if (cellValue != null) entry.values.add(cellValue);\n }\n }\n }\n\n // Build sorted output\n const result = new Map<string, unknown[]>();\n for (const [field, { values, hasBlank, hasExtractor }] of acc) {\n if (hasExtractor && hasBlank) values.add(BLANK_FILTER_VALUE);\n result.set(\n field,\n [...values].sort((a, b) => {\n if (typeof a === 'number' && typeof b === 'number') return a - b;\n return String(a).localeCompare(String(b));\n }),\n );\n }\n return result;\n}\n","/**\n * Row Virtualization Module\n *\n * Provides all virtualization-related functionality for the grid:\n *\n * 1. **Position Cache** (index-based): Maps row index → {offset, height, measured}\n * - Rebuilt when row count changes (expand/collapse, filter)\n * - Used for scroll position → row index lookups (binary search)\n *\n * 2. **Height Cache** (identity-based): Maps row identity → height\n * - Persists across expand/collapse\n * - Uses rowId string keys when available, WeakMap otherwise\n * - Synthetic rows use __rowCacheKey for stable identity\n *\n * 3. **Virtual Window**: Computes which rows to render based on scroll position\n *\n * 4. **Row Measurement**: Measures rendered row heights from DOM\n */\n\n// #region Types\n\n/**\n * Position entry for a single row in the position cache.\n * @category Plugin Development\n */\nexport interface RowPosition {\n /** Cumulative offset from top in pixels */\n offset: number;\n /** Row height in pixels */\n height: number;\n /** Whether this is a measured value (true) or estimate (false) */\n measured: boolean;\n}\n\n/**\n * Height cache that persists row heights across position cache rebuilds.\n * Uses dual storage: Map for string keys (rowId, __rowCacheKey) and WeakMap for object refs.\n * @category Plugin Development\n */\nexport interface HeightCache {\n /** Heights keyed by string (for synthetic rows with __rowCacheKey or rowId-keyed rows) */\n byKey: Map<string, number>;\n /** Heights keyed by object reference (for data rows without rowId) */\n byRef: WeakMap<object, number>;\n}\n\n/**\n * Configuration for the position cache.\n */\nexport interface PositionCacheConfig<T = unknown> {\n /** Function to get row ID (if configured) */\n rowId?: (row: T) => string | number;\n}\n\n// #endregion\n\n// #region Height Cache Operations\n\n/**\n * Create a new empty height cache.\n */\nexport function createHeightCache(): HeightCache {\n return {\n byKey: new Map(),\n byRef: new WeakMap(),\n };\n}\n\n/**\n * Get the cache key for a row.\n * Returns string for synthetic rows (__rowCacheKey) or rowId-keyed rows,\n * or the row object itself for WeakMap lookup.\n */\nexport function getRowCacheKey<T>(row: T, rowId?: (row: T) => string | number): string | T {\n if (!row || typeof row !== 'object') return row;\n\n // 1. Synthetic rows: plugins MUST add __rowCacheKey\n if ('__rowCacheKey' in row) {\n return (row as { __rowCacheKey: string }).__rowCacheKey;\n }\n\n // 2. rowId property directly on the row (common pattern)\n if ('rowId' in row && (row as { rowId: string | number }).rowId != null) {\n return `id:${(row as { rowId: string | number }).rowId}`;\n }\n\n // 3. User-provided rowId function (if configured)\n if (rowId) {\n return `id:${rowId(row)}`;\n }\n\n // 4. Object identity (for WeakMap)\n return row;\n}\n\n/**\n * Get cached height for a row.\n * @returns Cached height or undefined if not cached\n */\nexport function getCachedHeight<T>(\n cache: HeightCache,\n row: T,\n rowId?: (row: T) => string | number,\n): number | undefined {\n const key = getRowCacheKey(row, rowId);\n\n if (typeof key === 'string') {\n return cache.byKey.get(key);\n }\n\n // Object key - use WeakMap\n if (key && typeof key === 'object') {\n return cache.byRef.get(key);\n }\n\n return undefined;\n}\n\n/**\n * Set cached height for a row.\n */\nexport function setCachedHeight<T>(\n cache: HeightCache,\n row: T,\n height: number,\n rowId?: (row: T) => string | number,\n): void {\n const key = getRowCacheKey(row, rowId);\n\n if (typeof key === 'string') {\n cache.byKey.set(key, height);\n } else if (key && typeof key === 'object') {\n cache.byRef.set(key, height);\n }\n}\n\n// #endregion\n\n// #region Position Cache Operations\n\n/**\n * Rebuild position cache preserving known heights from height cache.\n * Called when row count changes (expand/collapse, filter, data change).\n *\n * @param rows - Array of row data\n * @param heightCache - Height cache with persisted measurements\n * @param estimatedHeight - Estimated height for unmeasured rows\n * @param config - Position cache configuration\n * @param getPluginHeight - Optional function to get height from plugins\n * @returns New position cache\n */\nexport function rebuildPositionCache<T>(\n rows: T[],\n heightCache: HeightCache,\n estimatedHeight: number,\n config: PositionCacheConfig<T>,\n getPluginHeight?: (row: T, index: number) => number | undefined,\n): RowPosition[] {\n const cache: RowPosition[] = new Array(rows.length);\n let offset = 0;\n\n for (let i = 0; i < rows.length; i++) {\n const row = rows[i];\n\n // Height resolution order:\n // 1. Plugin's getRowHeight() (for synthetic rows with known heights)\n let height = getPluginHeight?.(row, i);\n let measured = height !== undefined;\n\n // 2. Cached height from previous measurements\n if (height === undefined) {\n height = getCachedHeight(heightCache, row, config.rowId);\n measured = height !== undefined;\n }\n\n // 3. Fall back to estimate\n if (height === undefined) {\n height = estimatedHeight;\n measured = false;\n }\n\n cache[i] = { offset, height, measured };\n offset += height;\n }\n\n return cache;\n}\n\n/**\n * Update a single row's height in the position cache.\n * Recalculates offsets for all subsequent rows.\n *\n * @param cache - Position cache to update\n * @param index - Row index to update\n * @param newHeight - New measured height\n */\nexport function updateRowHeight(cache: RowPosition[], index: number, newHeight: number): void {\n if (index < 0 || index >= cache.length) return;\n\n const entry = cache[index];\n const heightDiff = newHeight - entry.height;\n\n if (heightDiff === 0) return;\n\n // Update this row\n entry.height = newHeight;\n entry.measured = true;\n\n // Recalculate offsets for all subsequent rows\n for (let i = index + 1; i < cache.length; i++) {\n cache[i].offset += heightDiff;\n }\n}\n\n/**\n * Get total content height from position cache.\n *\n * @param cache - Position cache\n * @returns Total height in pixels\n */\nexport function getTotalHeight(cache: RowPosition[]): number {\n if (cache.length === 0) return 0;\n const last = cache[cache.length - 1];\n return last.offset + last.height;\n}\n\n// #endregion\n\n// #region Binary Search\n\n/**\n * Find the row index at a given scroll offset using binary search.\n * Returns the index of the row that contains the given pixel offset.\n *\n * @param cache - Position cache\n * @param targetOffset - Scroll offset in pixels\n * @returns Row index at that offset, or -1 if cache is empty\n */\nexport function getRowIndexAtOffset(cache: RowPosition[], targetOffset: number): number {\n if (cache.length === 0) return -1;\n if (targetOffset <= 0) return 0;\n\n let low = 0;\n let high = cache.length - 1;\n\n while (low <= high) {\n const mid = Math.floor((low + high) / 2);\n const entry = cache[mid];\n const entryEnd = entry.offset + entry.height;\n\n if (targetOffset < entry.offset) {\n high = mid - 1;\n } else if (targetOffset >= entryEnd) {\n low = mid + 1;\n } else {\n // targetOffset is within this row\n return mid;\n }\n }\n\n // Return the closest row (low will be just past the target)\n return Math.max(0, Math.min(low, cache.length - 1));\n}\n\n// #endregion\n\n// #region Statistics\n\n/**\n * Calculate the average measured height.\n * Used for estimating unmeasured rows.\n *\n * @param cache - Position cache\n * @param defaultHeight - Default height to use if no measurements\n * @returns Average measured height\n */\nexport function calculateAverageHeight(cache: RowPosition[], defaultHeight: number): number {\n let totalHeight = 0;\n let measuredCount = 0;\n\n for (const entry of cache) {\n if (entry.measured) {\n totalHeight += entry.height;\n measuredCount++;\n }\n }\n\n return measuredCount > 0 ? totalHeight / measuredCount : defaultHeight;\n}\n\n/**\n * Count how many rows have been measured.\n *\n * @param cache - Position cache\n * @returns Number of measured rows\n */\nexport function countMeasuredRows(cache: RowPosition[]): number {\n let count = 0;\n for (const entry of cache) {\n if (entry.measured) count++;\n }\n return count;\n}\n\n// #endregion\n// #region Row Measurement\n\n/**\n * Result of measuring rendered row heights.\n */\nexport interface RowMeasurementResult {\n /** Whether any heights changed */\n hasChanges: boolean;\n /** Updated measured row count */\n measuredCount: number;\n /** Updated average height */\n averageHeight: number;\n}\n\n/**\n * Context for measuring rendered rows.\n */\nexport interface RowMeasurementContext<T> {\n /** Position cache to update */\n positionCache: RowPosition[];\n /** Height cache for persistence */\n heightCache: HeightCache;\n /** Row data array */\n rows: T[];\n /** Default row height */\n defaultHeight: number;\n /** Start index of rendered window */\n start: number;\n /** End index of rendered window (exclusive) */\n end: number;\n /** Function to get plugin height for a row */\n getPluginHeight?: (row: T, index: number) => number | undefined;\n /** Function to get row ID for height cache keying */\n getRowId?: (row: T) => string | number;\n}\n\n/**\n * Measure rendered row heights from DOM elements and update caches.\n * Returns measurement statistics for updating virtualization state.\n *\n * @param context - Measurement context with all dependencies\n * @param rowElements - NodeList of rendered row elements\n * @returns Measurement result with flags and statistics\n */\nexport function measureRenderedRowHeights<T>(\n context: RowMeasurementContext<T>,\n rowElements: NodeListOf<Element>,\n): RowMeasurementResult {\n const { positionCache, heightCache, rows, start, end, getPluginHeight, getRowId } = context;\n\n let hasChanges = false;\n\n rowElements.forEach((rowEl) => {\n const rowIndexStr = (rowEl as HTMLElement).dataset.rowIndex;\n if (!rowIndexStr) return;\n\n const rowIndex = parseInt(rowIndexStr, 10);\n if (rowIndex < start || rowIndex >= end || rowIndex >= rows.length) return;\n\n const row = rows[rowIndex];\n\n // Check if a plugin provides the height for this row\n const pluginHeight = getPluginHeight?.(row, rowIndex);\n\n if (pluginHeight !== undefined) {\n // Plugin provides height - use it for position cache\n const currentEntry = positionCache[rowIndex];\n if (!currentEntry.measured || Math.abs(currentEntry.height - pluginHeight) > 1) {\n updateRowHeight(positionCache, rowIndex, pluginHeight);\n hasChanges = true;\n }\n return; // Don't measure DOM for plugin-managed rows\n }\n\n // No plugin height - use DOM measurement\n const measuredHeight = (rowEl as HTMLElement).offsetHeight;\n\n if (measuredHeight > 0) {\n const currentEntry = positionCache[rowIndex];\n\n // Only update if height differs significantly (> 1px to avoid oscillation)\n if (!currentEntry.measured || Math.abs(currentEntry.height - measuredHeight) > 1) {\n updateRowHeight(positionCache, rowIndex, measuredHeight);\n setCachedHeight(heightCache, row, measuredHeight, getRowId);\n hasChanges = true;\n }\n }\n });\n\n // Recompute stats\n const measuredCount = hasChanges ? countMeasuredRows(positionCache) : 0;\n const averageHeight = hasChanges ? calculateAverageHeight(positionCache, context.defaultHeight) : 0;\n\n return { hasChanges, measuredCount, averageHeight };\n}\n\n/**\n * Compute measurement statistics for a position cache, excluding plugin-managed rows.\n * Used after rebuilding position cache to get accurate averages for non-plugin rows.\n *\n * @param cache - Position cache\n * @param rows - Row data array\n * @param defaultHeight - Default height if no measurements\n * @param getPluginHeight - Function to check if plugin manages row height\n * @returns Object with measured count and average height\n */\nexport function computeAverageExcludingPluginRows<T>(\n cache: RowPosition[],\n rows: T[],\n defaultHeight: number,\n getPluginHeight?: (row: T, index: number) => number | undefined,\n): { measuredCount: number; averageHeight: number } {\n let measuredCount = 0;\n let totalMeasured = 0;\n\n for (let i = 0; i < cache.length; i++) {\n const entry = cache[i];\n if (entry.measured) {\n // Only include in average if plugin doesn't provide height for this row\n const pluginHeight = getPluginHeight?.(rows[i], i);\n if (pluginHeight === undefined) {\n totalMeasured += entry.height;\n measuredCount++;\n }\n }\n }\n\n return {\n measuredCount,\n averageHeight: measuredCount > 0 ? totalMeasured / measuredCount : defaultHeight,\n };\n}\n\n// #endregion\n\n// #region Fixed-Height Virtual Window\n\n/**\n * Result of computing a virtual window for fixed-height rows.\n * Used by plugins like FilteringPlugin for virtualized dropdowns.\n */\nexport interface VirtualWindow {\n /** First row index to render (inclusive) */\n start: number;\n /** Last row index to render (exclusive) */\n end: number;\n /** Pixel offset to apply to the rows container (translateY) */\n offsetY: number;\n /** Total height of the scrollable content */\n totalHeight: number;\n}\n\n/** Parameters for computing the virtual window */\nexport interface VirtualWindowParams {\n /** Total number of rows */\n totalRows: number;\n /** Height of the viewport in pixels */\n viewportHeight: number;\n /** Current scroll top position */\n scrollTop: number;\n /** Height of each row in pixels */\n rowHeight: number;\n /** Number of extra rows to render above/below viewport */\n overscan: number;\n}\n\n/**\n * Compute the virtual row window based on scroll position and viewport.\n * Simple fixed-height implementation for plugins needing basic virtualization.\n *\n * @param params - Parameters for computing the window\n * @returns VirtualWindow with start/end indices and transforms\n */\nexport function computeVirtualWindow(params: VirtualWindowParams): VirtualWindow {\n const { totalRows, viewportHeight, scrollTop, rowHeight, overscan } = params;\n\n const visibleRows = Math.ceil(viewportHeight / rowHeight);\n\n // Render overscan rows in each direction (total window = visible + 2*overscan)\n let start = Math.floor(scrollTop / rowHeight) - overscan;\n if (start < 0) start = 0;\n\n let end = start + visibleRows + overscan * 2;\n if (end > totalRows) end = totalRows;\n\n // Ensure start is adjusted if we hit the end\n if (end === totalRows && start > 0) {\n start = Math.max(0, end - visibleRows - overscan * 2);\n }\n\n return {\n start,\n end,\n offsetY: start * rowHeight,\n totalHeight: totalRows * rowHeight,\n };\n}\n\n/**\n * Determine if virtualization should be bypassed for small datasets.\n * When there are very few items, the overhead of virtualization isn't worth it.\n *\n * @param totalRows - Total number of items\n * @param threshold - Bypass threshold (skip virtualization if totalRows <= threshold)\n * @returns True if virtualization should be bypassed\n */\nexport function shouldBypassVirtualization(totalRows: number, threshold: number): boolean {\n return totalRows <= threshold;\n}\n\n// #endregion\n","import type { RowPosition } from './internal/virtualization';\nimport type { PluginQuery } from './plugin/base-plugin';\nimport type { AfterCellRenderContext, AfterRowRenderContext, CellMouseEvent } from './plugin/types';\n\n/**\n * Position entry for a single row in the position cache.\n * Part of variable row height virtualization.\n *\n * Re-exported from position-cache.ts for public API consistency.\n *\n * @see VirtualState.positionCache\n * @category Plugin Development\n */\nexport type RowPositionEntry = RowPosition;\n\n// #region DataGridElement Interface\n/**\n * The compiled web component interface for DataGrid.\n *\n * This interface represents the `<tbw-grid>` custom element, combining\n * the public grid API with standard HTMLElement functionality.\n *\n * @example\n * ```typescript\n * // Query existing grid\n * const grid = document.querySelector('tbw-grid') as DataGridElement<Employee>;\n * grid.rows = employees;\n * grid.addEventListener('cell-click', (e) => console.log(e.detail));\n *\n * // Create grid programmatically\n * import { createGrid } from '@toolbox-web/grid';\n * const grid = createGrid<Employee>({\n * columns: [{ field: 'name' }, { field: 'email' }],\n * });\n * document.body.appendChild(grid);\n * ```\n *\n * @see {@link PublicGrid} for the public API methods and properties\n * @see {@link createGrid} for typed grid creation\n * @see {@link queryGrid} for typed grid querying\n */\nexport interface DataGridElement extends PublicGrid, HTMLElement {}\n// #endregion\n\n// #region PublicGrid Interface\n/**\n * Public API interface for DataGrid component.\n *\n * **Property Getters vs Setters:**\n *\n * Property getters return the EFFECTIVE (resolved) value after merging all config sources.\n * This is the \"current situation\" - what consumers and plugins need to know.\n *\n * Property setters accept input values which are merged into the effective config.\n * Multiple sources can contribute (gridConfig, columns prop, light DOM, individual props).\n *\n * For example:\n * - `grid.fitMode` returns the resolved fitMode (e.g., 'stretch' even if you set undefined)\n * - `grid.columns` returns the effective columns after merging\n * - `grid.gridConfig` returns the full effective config\n */\nexport interface PublicGrid<T = any> {\n /**\n * Full config object. Setter merges with other inputs per precedence rules.\n * Getter returns the effective (resolved) config.\n */\n gridConfig?: GridConfig<T>;\n /**\n * Column definitions.\n * Getter returns effective columns (after merging config, light DOM, inference).\n */\n columns?: ColumnConfig<T>[];\n /** Current row data (after plugin processing like grouping, filtering). */\n rows?: T[];\n /** Resolves once the component has finished initial work (layout, inference). */\n ready?: () => Promise<void>;\n /** Force a layout / measurement pass (e.g. after container resize). */\n forceLayout?: () => Promise<void>;\n /** Return effective resolved config (after inference & precedence). */\n getConfig?: () => Promise<Readonly<GridConfig<T>>>;\n /** Toggle expansion state of a group row by its generated key. */\n toggleGroup?: (key: string) => Promise<void>;\n\n // Custom Styles API\n /**\n * Register custom CSS styles to be injected into the grid.\n * Use this to style custom cell renderers, editors, or detail panels.\n * @param id - Unique identifier for the style block (for removal/updates)\n * @param css - CSS string to inject\n */\n registerStyles?: (id: string, css: string) => void;\n /**\n * Remove previously registered custom styles.\n * @param id - The ID used when registering the styles\n */\n unregisterStyles?: (id: string) => void;\n /**\n * Get list of registered custom style IDs.\n */\n getRegisteredStyles?: () => string[];\n\n // Plugin API\n /**\n * Get a plugin instance by its class.\n *\n * @example\n * ```typescript\n * const selection = grid.getPlugin(SelectionPlugin);\n * if (selection) {\n * selection.selectAll();\n * }\n * ```\n */\n getPlugin?<P>(PluginClass: new (...args: any[]) => P): P | undefined;\n /**\n * Get a plugin instance by its name.\n * Prefer `getPlugin(PluginClass)` for type safety.\n */\n getPluginByName?(name: string): GridPlugin | undefined;\n\n // Shell API\n /**\n * Re-render the shell header (title, column groups, toolbar).\n * Call this after dynamically adding/removing tool panels or toolbar buttons.\n */\n refreshShellHeader?(): void;\n /**\n * Register a custom tool panel in the sidebar.\n *\n * @example\n * ```typescript\n * grid.registerToolPanel({\n * id: 'analytics',\n * title: 'Analytics',\n * icon: '📊',\n * render: (container) => {\n * container.innerHTML = '<div>Charts here...</div>';\n * }\n * });\n * ```\n */\n registerToolPanel?(panel: ToolPanelDefinition): void;\n /**\n * Unregister a previously registered tool panel.\n */\n unregisterToolPanel?(panelId: string): void;\n /**\n * Open the tool panel sidebar.\n */\n openToolPanel?(): void;\n /**\n * Close the tool panel sidebar.\n */\n closeToolPanel?(): void;\n /**\n * Toggle the tool panel sidebar open or closed.\n */\n toggleToolPanel?(): void;\n /**\n * Toggle an accordion section expanded or collapsed within the tool panel.\n * @param sectionId - The ID of the section to toggle\n */\n toggleToolPanelSection?(sectionId: string): void;\n\n // State Persistence API\n /**\n * Get the current column state including order, width, visibility, and sort.\n * Use for persisting user preferences to localStorage or a backend.\n *\n * @example\n * ```typescript\n * const state = grid.getColumnState();\n * localStorage.setItem('gridState', JSON.stringify(state));\n * ```\n */\n getColumnState?(): GridColumnState;\n /**\n * Set/restore the column state.\n * Can be set before or after grid initialization.\n *\n * @example\n * ```typescript\n * const saved = localStorage.getItem('gridState');\n * if (saved) {\n * grid.columnState = JSON.parse(saved);\n * }\n * ```\n */\n columnState?: GridColumnState;\n\n // Loading API\n /**\n * Whether the grid is currently in a loading state.\n * When true, displays a loading overlay with spinner.\n *\n * Can also be set via the `loading` HTML attribute.\n *\n * @example\n * ```typescript\n * // Show loading overlay\n * grid.loading = true;\n * const data = await fetchData();\n * grid.rows = data;\n * grid.loading = false;\n * ```\n */\n loading?: boolean;\n\n /**\n * Set loading state for a specific row.\n * Displays a small spinner indicator on the row.\n *\n * Use when persisting row data or performing row-level async operations.\n *\n * @param rowId - The row's unique identifier (from getRowId)\n * @param loading - Whether the row is loading\n *\n * @example\n * ```typescript\n * // Show loading while saving row\n * grid.setRowLoading('emp-123', true);\n * await saveRow(row);\n * grid.setRowLoading('emp-123', false);\n * ```\n */\n setRowLoading?(rowId: string, loading: boolean): void;\n\n /**\n * Set loading state for a specific cell.\n * Displays a small spinner indicator on the cell.\n *\n * Use when performing cell-level async operations (e.g., validation, lookup).\n *\n * @param rowId - The row's unique identifier (from getRowId)\n * @param field - The column field\n * @param loading - Whether the cell is loading\n *\n * @example\n * ```typescript\n * // Show loading while validating cell\n * grid.setCellLoading('emp-123', 'email', true);\n * const isValid = await validateEmail(email);\n * grid.setCellLoading('emp-123', 'email', false);\n * ```\n */\n setCellLoading?(rowId: string, field: string, loading: boolean): void;\n\n /**\n * Check if a row is currently in loading state.\n * @param rowId - The row's unique identifier\n */\n isRowLoading?(rowId: string): boolean;\n\n /**\n * Check if a cell is currently in loading state.\n * @param rowId - The row's unique identifier\n * @param field - The column field\n */\n isCellLoading?(rowId: string, field: string): boolean;\n\n /**\n * Clear all row and cell loading states.\n */\n clearAllLoading?(): void;\n}\n// #endregion\n\n// #region InternalGrid Interface\n/**\n * Internal-only augmented interface for DataGrid component.\n *\n * Member prefixes indicate accessibility:\n * - `_underscore` = protected members - private outside core, accessible to plugins. Marked with @internal.\n * - `__doubleUnderscore` = deeply internal members - private outside core, only for internal functions.\n *\n * @category Plugin Development\n */\nexport interface InternalGrid<T = any> extends PublicGrid<T>, GridConfig<T> {\n // Element methods available because DataGridElement extends HTMLElement\n querySelector<K extends keyof HTMLElementTagNameMap>(selectors: K): HTMLElementTagNameMap[K] | null;\n querySelector<E extends Element = Element>(selectors: string): E | null;\n querySelectorAll<K extends keyof HTMLElementTagNameMap>(selectors: K): NodeListOf<HTMLElementTagNameMap[K]>;\n querySelectorAll<E extends Element = Element>(selectors: string): NodeListOf<E>;\n _rows: T[];\n _columns: ColumnInternal<T>[];\n /** Visible columns only (excludes hidden). Use for rendering. */\n _visibleColumns: ColumnInternal<T>[];\n _headerRowEl: HTMLElement;\n _bodyEl: HTMLElement;\n _rowPool: RowElementInternal[];\n _resizeController: ResizeController;\n _sortState: { field: string; direction: 1 | -1 } | null;\n /** Original unfiltered/unprocessed rows. @internal */\n sourceRows: T[];\n /** Framework adapter instance (set by Grid directives). @internal */\n __frameworkAdapter?: FrameworkAdapter;\n __originalOrder: T[];\n __rowRenderEpoch: number;\n __didInitialAutoSize?: boolean;\n __lightDomColumnsCache?: ColumnInternal[];\n __originalColumnNodes?: HTMLElement[];\n /** Cell display value cache. @internal */\n __cellDisplayCache?: Map<number, string[]>;\n /** Cache epoch for cell display values. @internal */\n __cellCacheEpoch?: number;\n /** Cached header row count for virtualization. @internal */\n __cachedHeaderRowCount?: number;\n /** Cached flag for whether grid has special columns (custom renderers, etc.). @internal */\n __hasSpecialColumns?: boolean;\n /** Cached flag for whether any plugin has renderRow hooks. @internal */\n __hasRenderRowPlugins?: boolean;\n _gridTemplate: string;\n _virtualization: VirtualState;\n _focusRow: number;\n _focusCol: number;\n /** Currently active edit row index. Injected by EditingPlugin. @internal */\n _activeEditRows?: number;\n /** Whether the grid is in 'grid' editing mode (all rows editable). Injected by EditingPlugin. @internal */\n _isGridEditMode?: boolean;\n /** Snapshots of row data before editing. Injected by EditingPlugin. @internal */\n _rowEditSnapshots?: Map<number, T>;\n /** Get all changed rows. Injected by EditingPlugin. */\n changedRows?: T[];\n /** Get IDs of all changed rows. Injected by EditingPlugin. */\n changedRowIds?: string[];\n effectiveConfig?: GridConfig<T>;\n findHeaderRow?: () => HTMLElement;\n refreshVirtualWindow: (full: boolean, skipAfterRender?: boolean) => boolean;\n updateTemplate?: () => void;\n findRenderedRowElement?: (rowIndex: number) => HTMLElement | null;\n /** Get a row by its ID. Implemented in grid.ts */\n getRow?: (id: string) => T | undefined;\n /** Get a row and its current index by ID. Returns undefined if not found. @internal */\n _getRowEntry: (id: string) => { row: T; index: number } | undefined;\n /** Get the unique ID for a row. Implemented in grid.ts */\n getRowId?: (row: T) => string;\n /** Update a row by ID. Implemented in grid.ts */\n updateRow?: (id: string, changes: Partial<T>, source?: UpdateSource) => void;\n /** Animate a single row. Implemented in grid.ts */\n animateRow?: (rowIndex: number, type: RowAnimationType) => void;\n /** Animate multiple rows. Implemented in grid.ts */\n animateRows?: (rowIndices: number[], type: RowAnimationType) => void;\n /** Animate a row by its ID. Implemented in grid.ts */\n animateRowById?: (rowId: string, type: RowAnimationType) => boolean;\n /** Begin bulk edit on a row. Injected by EditingPlugin. */\n beginBulkEdit?: (rowIndex: number) => void;\n /** Commit active row edit. Injected by EditingPlugin. */\n commitActiveRowEdit?: () => void;\n /** Dispatch cell click to plugin system, returns true if handled */\n _dispatchCellClick?: (event: MouseEvent, rowIndex: number, colIndex: number, cellEl: HTMLElement) => boolean;\n /** Dispatch row click to plugin system, returns true if handled */\n _dispatchRowClick?: (event: MouseEvent, rowIndex: number, row: any, rowEl: HTMLElement) => boolean;\n /** Dispatch header click to plugin system, returns true if handled */\n _dispatchHeaderClick?: (event: MouseEvent, col: ColumnConfig, headerEl: HTMLElement) => boolean;\n /** Dispatch keydown to plugin system, returns true if handled */\n _dispatchKeyDown?: (event: KeyboardEvent) => boolean;\n /** Dispatch cell mouse events for drag operations. Returns true if any plugin started a drag. */\n _dispatchCellMouseDown?: (event: CellMouseEvent) => boolean;\n /** Dispatch cell mouse move during drag. */\n _dispatchCellMouseMove?: (event: CellMouseEvent) => void;\n /** Dispatch cell mouse up to end drag. */\n _dispatchCellMouseUp?: (event: CellMouseEvent) => void;\n /** Call afterCellRender hook on all plugins. Called from rows.ts after each cell is rendered. @internal */\n _afterCellRender?: (context: AfterCellRenderContext<T>) => void;\n /** Check if any plugin has registered an afterCellRender hook. Used to skip hook call for performance. @internal */\n _hasAfterCellRenderHook?: () => boolean;\n /** Call afterRowRender hook on all plugins. Called from rows.ts after each row is rendered. @internal */\n _afterRowRender?: (context: AfterRowRenderContext<T>) => void;\n /** Check if any plugin has registered an afterRowRender hook. Used to skip hook call for performance. @internal */\n _hasAfterRowRenderHook?: () => boolean;\n /** Get horizontal scroll boundary offsets from plugins */\n _getHorizontalScrollOffsets?: (\n rowEl?: HTMLElement,\n focusedCell?: HTMLElement,\n ) => { left: number; right: number; skipScroll?: boolean };\n /** Query all plugins with a generic query and collect responses */\n queryPlugins?: <T>(query: PluginQuery) => T[];\n /** Request emission of column-state-change event (debounced) */\n requestStateChange?: () => void;\n}\n// #endregion\n\n// #region Column Types\n/**\n * Built-in primitive column types with automatic formatting and editing support.\n *\n * - `'string'` - Text content, default text input editor\n * - `'number'` - Numeric content, right-aligned, number input editor\n * - `'date'` - Date content, formatted display, date picker editor\n * - `'boolean'` - True/false, rendered as checkbox\n * - `'select'` - Dropdown selection from `options` array\n *\n * @example\n * ```typescript\n * columns: [\n * { field: 'name', type: 'string' },\n * { field: 'age', type: 'number' },\n * { field: 'hireDate', type: 'date' },\n * { field: 'active', type: 'boolean' },\n * { field: 'department', type: 'select', options: [\n * { label: 'Engineering', value: 'eng' },\n * { label: 'Sales', value: 'sales' },\n * ]},\n * ]\n * ```\n *\n * @see {@link ColumnType} for custom type support\n * @see {@link TypeDefault} for type-level defaults\n */\nexport type PrimitiveColumnType = 'number' | 'string' | 'date' | 'boolean' | 'select';\n\n/**\n * Column type - built-in primitives or custom type strings.\n *\n * Use built-in types for automatic formatting, or define custom types\n * (e.g., 'currency', 'country') with type-level defaults via `typeDefaults`.\n *\n * @example\n * ```typescript\n * // Built-in types\n * { field: 'name', type: 'string' }\n * { field: 'salary', type: 'number' }\n *\n * // Custom types with defaults\n * grid.gridConfig = {\n * columns: [\n * { field: 'salary', type: 'currency' },\n * { field: 'birthCountry', type: 'country' },\n * ],\n * typeDefaults: {\n * currency: {\n * format: (v) => `$${Number(v).toFixed(2)}`,\n * },\n * country: {\n * renderer: (ctx) => `🌍 ${ctx.value}`,\n * },\n * },\n * };\n * ```\n *\n * @see {@link PrimitiveColumnType} for built-in types\n * @see {@link TypeDefault} for defining custom type defaults\n */\nexport type ColumnType = PrimitiveColumnType | (string & {});\n// #endregion\n\n// #region TypeDefault Interface\n/**\n * Type-level defaults for formatters and renderers.\n * Applied to all columns of a given type unless overridden at column level.\n *\n * Note: `editor` and `editorParams` are added via module augmentation when\n * EditingPlugin is imported. `filterPanelRenderer` is added by FilteringPlugin.\n *\n * @example\n * ```typescript\n * typeDefaults: {\n * currency: {\n * format: (value) => new Intl.NumberFormat('en-US', {\n * style: 'currency',\n * currency: 'USD',\n * }).format(value as number),\n * },\n * country: {\n * renderer: (ctx) => {\n * const span = document.createElement('span');\n * span.innerHTML = `<img src=\"/flags/${ctx.value}.svg\" /> ${ctx.value}`;\n * return span;\n * }\n * }\n * }\n * ```\n *\n * @see {@link ColumnViewRenderer} for custom renderer function signature\n * @see {@link ColumnType} for type strings that can have defaults\n * @see {@link GridConfig.typeDefaults} for registering type defaults\n */\nexport interface TypeDefault<TRow = unknown> {\n /**\n * Default formatter for all columns of this type.\n *\n * Transforms the raw cell value into a display string. Use when you need\n * consistent formatting across columns without custom DOM (e.g., currency,\n * percentages, dates with specific locale).\n *\n * **Resolution Priority**: Column `format` → Type `format` → Built-in\n *\n * @example\n * ```typescript\n * typeDefaults: {\n * currency: {\n * format: (value) => new Intl.NumberFormat('en-US', {\n * style: 'currency',\n * currency: 'USD',\n * }).format(value as number),\n * },\n * percentage: {\n * format: (value) => `${(value as number * 100).toFixed(1)}%`,\n * }\n * }\n * ```\n */\n format?: (value: unknown, row: TRow) => string;\n\n /**\n * Default renderer for all columns of this type.\n *\n * Creates custom DOM for the cell content. Use when you need more than\n * text formatting (e.g., icons, badges, interactive elements).\n *\n * **Resolution Priority**: Column `renderer` → Type `renderer` → App-level (adapter) → Built-in\n *\n * @example\n * ```typescript\n * typeDefaults: {\n * status: {\n * renderer: (ctx) => {\n * const badge = document.createElement('span');\n * badge.className = `badge badge-${ctx.value}`;\n * badge.textContent = ctx.value as string;\n * return badge;\n * }\n * },\n * country: {\n * renderer: (ctx) => {\n * const span = document.createElement('span');\n * span.innerHTML = `<img src=\"/flags/${ctx.value}.svg\" /> ${ctx.value}`;\n * return span;\n * }\n * }\n * }\n * ```\n */\n renderer?: ColumnViewRenderer<TRow, unknown>;\n}\n// #endregion\n\n// #region BaseColumnConfig Interface\n/**\n * Base contract for a column configuration.\n *\n * Defines the fundamental properties all columns share. Extended by {@link ColumnConfig}\n * with additional features like custom renderers and grouping.\n *\n * @example\n * ```typescript\n * // Basic column with common properties\n * const columns: BaseColumnConfig<Employee>[] = [\n * {\n * field: 'name',\n * header: 'Full Name',\n * sortable: true,\n * resizable: true,\n * },\n * {\n * field: 'salary',\n * type: 'number',\n * width: 120,\n * format: (value) => `$${value.toLocaleString()}`,\n * sortComparator: (a, b) => a - b,\n * },\n * {\n * field: 'department',\n * type: 'select',\n * options: [\n * { label: 'Engineering', value: 'eng' },\n * { label: 'Sales', value: 'sales' },\n * ],\n * },\n * ];\n * ```\n *\n * @see {@link ColumnConfig} for full column configuration with renderers\n * @see {@link ColumnType} for type options\n */\nexport interface BaseColumnConfig<TRow = any, TValue = any> {\n /** Unique field key referencing property in row objects */\n field: keyof TRow & string;\n /** Visible header label; defaults to capitalized field */\n header?: string;\n /**\n * Column data type.\n *\n * Built-in types: `'string'`, `'number'`, `'date'`, `'boolean'`, `'select'`\n *\n * Custom types (e.g., `'currency'`, `'country'`) can have type-level defaults\n * via `gridConfig.typeDefaults` or framework adapter registries.\n *\n * @default Inferred from first row data\n */\n type?: ColumnType;\n /** Column width in pixels; fixed size (no flexibility) */\n width?: string | number;\n /** Minimum column width in pixels (stretch mode only); when set, column uses minmax(minWidth, 1fr) */\n minWidth?: number;\n /** Whether column can be sorted */\n sortable?: boolean;\n /** Whether column can be resized by user */\n resizable?: boolean;\n /** Optional custom comparator for sorting (a,b) -> number */\n sortComparator?: (a: TValue, b: TValue, rowA: TRow, rowB: TRow) => number;\n /** For select type - available options */\n options?: Array<{ label: string; value: unknown }> | (() => Array<{ label: string; value: unknown }>);\n /** For select - allow multi select */\n multi?: boolean;\n /**\n * Formats the raw cell value into a display string.\n *\n * Used both for **cell rendering** and the **built-in filter panel**:\n * - In cells, the formatted value replaces the raw value as text content.\n * - In the filter panel (set filter), checkbox labels show the formatted value\n * instead of the raw value, and search matches against the formatted text.\n *\n * The `row` parameter is available during cell rendering but is `undefined`\n * when called from the filter panel (standalone value formatting). Avoid\n * accessing `row` properties in format functions intended for filter display.\n *\n * @example\n * ```typescript\n * // Currency formatter — works in both cells and filter panel\n * {\n * field: 'price',\n * format: (value) => `$${Number(value).toFixed(2)}`,\n * }\n *\n * // ID-to-name lookup — filter panel shows names, not IDs\n * {\n * field: 'departmentId',\n * format: (value) => departmentMap.get(value as string) ?? String(value),\n * }\n * ```\n */\n format?: (value: TValue, row: TRow) => string;\n /** Arbitrary extra metadata */\n meta?: Record<string, unknown>;\n}\n// #endregion\n\n// #region ColumnConfig Interface\n/**\n * Full column configuration including custom renderers, editors, and grouping metadata.\n *\n * Extends {@link BaseColumnConfig} with additional features for customizing\n * how cells are displayed and edited.\n *\n * @example\n * ```typescript\n * const columns: ColumnConfig<Employee>[] = [\n * // Basic sortable column\n * { field: 'id', header: 'ID', width: 60, sortable: true },\n *\n * // Column with custom renderer\n * {\n * field: 'name',\n * header: 'Employee',\n * renderer: (ctx) => {\n * const div = document.createElement('div');\n * div.innerHTML = `<img src=\"${ctx.row.avatar}\" /><span>${ctx.value}</span>`;\n * return div;\n * },\n * },\n *\n * // Column with custom header\n * {\n * field: 'email',\n * headerLabelRenderer: (ctx) => `${ctx.value} 📧`,\n * },\n *\n * // Editable column (requires EditingPlugin)\n * {\n * field: 'status',\n * editable: true,\n * editor: (ctx) => {\n * const select = document.createElement('select');\n * // ... editor implementation\n * return select;\n * },\n * },\n *\n * // Hidden column (can be shown via VisibilityPlugin)\n * { field: 'internalNotes', hidden: true },\n * ];\n * ```\n *\n * @see {@link BaseColumnConfig} for basic column properties\n * @see {@link ColumnViewRenderer} for custom cell renderers\n * @see {@link ColumnEditorSpec} for custom cell editors\n * @see {@link HeaderRenderer} for custom header renderers\n */\nexport interface ColumnConfig<TRow = any> extends BaseColumnConfig<TRow, any> {\n /**\n * Optional custom cell renderer function. Alias for `viewRenderer`.\n * Can return an HTMLElement, a Node, or an HTML string (which will be sanitized).\n *\n * @example\n * ```typescript\n * // Simple string template\n * renderer: (ctx) => `<span class=\"badge\">${ctx.value}</span>`\n *\n * // DOM element\n * renderer: (ctx) => {\n * const el = document.createElement('span');\n * el.textContent = ctx.value;\n * return el;\n * }\n * ```\n */\n renderer?: ColumnViewRenderer<TRow, any>;\n /** Optional custom view renderer used instead of default text rendering */\n viewRenderer?: ColumnViewRenderer<TRow, any>;\n /** External view spec (lets host app mount any framework component) */\n externalView?: {\n component: unknown;\n props?: Record<string, unknown>;\n mount?: (options: {\n placeholder: HTMLElement;\n context: CellRenderContext<TRow, unknown>;\n spec: unknown;\n }) => void | { dispose?: () => void };\n };\n /** Whether the column is initially hidden */\n hidden?: boolean;\n /** Prevent this column from being hidden programmatically */\n lockVisible?: boolean;\n /**\n * Dynamic CSS class(es) for cells in this column.\n * Called for each cell during rendering. Return class names to add to the cell element.\n *\n * @example\n * ```typescript\n * // Highlight negative values\n * cellClass: (value, row, column) => value < 0 ? ['negative', 'text-red'] : []\n *\n * // Status-based styling\n * cellClass: (value) => [`status-${value}`]\n * ```\n */\n cellClass?: (value: unknown, row: TRow, column: ColumnConfig<TRow>) => string[];\n\n /**\n * Custom header label renderer. Customize the label content while the grid\n * handles sort icons, filter buttons, resize handles, and click interactions.\n *\n * Use this for simple customizations like adding icons, badges, or units.\n *\n * @example\n * ```typescript\n * // Add required field indicator\n * headerLabelRenderer: (ctx) => `${ctx.value} <span class=\"required\">*</span>`\n *\n * // Add unit to header\n * headerLabelRenderer: (ctx) => {\n * const span = document.createElement('span');\n * span.innerHTML = `${ctx.value}<br/><small>(kg)</small>`;\n * return span;\n * }\n * ```\n */\n headerLabelRenderer?: HeaderLabelRenderer<TRow>;\n\n /**\n * Custom header cell renderer. Complete control over the entire header cell.\n * Resize handles are added automatically for resizable columns.\n *\n * The context provides helper functions to include standard elements:\n * - `renderSortIcon()` - Returns sort indicator element (null if not sortable)\n * - `renderFilterButton()` - Returns filter button (null if not filterable)\n *\n * **Precedence**: `headerRenderer` > `headerLabelRenderer` > `header` > `field`\n *\n * @example\n * ```typescript\n * headerRenderer: (ctx) => {\n * const div = document.createElement('div');\n * div.className = 'custom-header';\n * div.innerHTML = `<span>${ctx.value}</span>`;\n * const sortIcon = ctx.renderSortIcon();\n * if (sortIcon) div.appendChild(sortIcon);\n * return div;\n * }\n * ```\n */\n headerRenderer?: HeaderRenderer<TRow>;\n}\n// #endregion\n\n// #region ColumnConfigMap Type\n/**\n * Array of column configurations.\n * Convenience type alias for `ColumnConfig<TRow>[]`.\n *\n * @example\n * ```typescript\n * const columns: ColumnConfigMap<Employee> = [\n * { field: 'name', header: 'Full Name', sortable: true },\n * { field: 'email', header: 'Email Address' },\n * { field: 'department', type: 'select', options: deptOptions },\n * ];\n *\n * grid.columns = columns;\n * ```\n *\n * @see {@link ColumnConfig} for individual column options\n * @see {@link GridConfig.columns} for setting columns on the grid\n */\nexport type ColumnConfigMap<TRow = any> = ColumnConfig<TRow>[];\n// #endregion\n\n// #region Editor Types\n/**\n * Editor specification for inline cell editing.\n * Supports multiple formats for maximum flexibility.\n *\n * **Format Options:**\n * - `string` - Custom element tag name (e.g., 'my-date-picker')\n * - `function` - Factory function returning an editor element\n * - `object` - External component spec for framework integration\n *\n * @example\n * ```typescript\n * // 1. Custom element tag name\n * columns: [\n * { field: 'date', editor: 'my-date-picker' }\n * ]\n *\n * // 2. Factory function (full control)\n * columns: [\n * {\n * field: 'status',\n * editor: (ctx) => {\n * const select = document.createElement('select');\n * select.innerHTML = `\n * <option value=\"active\">Active</option>\n * <option value=\"inactive\">Inactive</option>\n * `;\n * select.value = ctx.value;\n * select.onchange = () => ctx.commit(select.value);\n * select.onkeydown = (e) => {\n * if (e.key === 'Escape') ctx.cancel();\n * };\n * return select;\n * }\n * }\n * ]\n *\n * // 3. External component (React, Angular, Vue)\n * columns: [\n * {\n * field: 'country',\n * editor: {\n * component: CountrySelect,\n * props: { showFlags: true }\n * }\n * }\n * ]\n * ```\n *\n * @see {@link ColumnEditorContext} for the context passed to factory functions\n */\nexport type ColumnEditorSpec<TRow = unknown, TValue = unknown> =\n | string // custom element tag name\n | ((context: ColumnEditorContext<TRow, TValue>) => HTMLElement | string)\n | {\n /** Arbitrary component reference (class, function, token) */\n component: unknown;\n /** Optional static props passed to mount */\n props?: Record<string, unknown>;\n /** Optional custom mount function; if provided we call it directly instead of emitting an event */\n mount?: (options: {\n placeholder: HTMLElement;\n context: ColumnEditorContext<TRow, TValue>;\n spec: unknown;\n }) => void | { dispose?: () => void };\n };\n\n/**\n * Context object provided to editor factories allowing mutation (commit/cancel) of a cell value.\n *\n * The `commit` and `cancel` functions control the editing lifecycle:\n * - Call `commit(newValue)` to save changes and exit edit mode\n * - Call `cancel()` to discard changes and exit edit mode\n *\n * @example\n * ```typescript\n * const myEditor: ColumnEditorSpec = (ctx: ColumnEditorContext) => {\n * const input = document.createElement('input');\n * input.value = ctx.value;\n * input.className = 'my-editor';\n *\n * // Save on Enter, cancel on Escape\n * input.onkeydown = (e) => {\n * if (e.key === 'Enter') {\n * ctx.commit(input.value);\n * } else if (e.key === 'Escape') {\n * ctx.cancel();\n * }\n * };\n *\n * // Access row data for validation\n * if (ctx.row.locked) {\n * input.disabled = true;\n * }\n *\n * return input;\n * };\n * ```\n *\n * @see {@link ColumnEditorSpec} for editor specification options\n */\nexport interface ColumnEditorContext<TRow = any, TValue = any> {\n /** Underlying full row object for the active edit. */\n row: TRow;\n /** Current cell value (mutable only via commit). */\n value: TValue;\n /** Field name being edited. */\n field: keyof TRow & string;\n /** Column configuration reference. */\n column: ColumnConfig<TRow>;\n /**\n * Stable row identifier (from `getRowId`).\n * Empty string if no `getRowId` is configured.\n */\n rowId: string;\n /** Accept the edit; triggers change tracking + rerender. */\n commit: (newValue: TValue) => void;\n /** Abort edit without persisting changes. */\n cancel: () => void;\n /**\n * Update other fields in this row while the editor is open.\n * Changes are committed with source `'cascade'`, triggering\n * `cell-change` events and `onValueChange` pushes to sibling editors.\n *\n * Useful for editors that affect multiple fields (e.g., an address\n * lookup that sets city + zip + state).\n *\n * @example\n * ```typescript\n * // In a cell-commit listener:\n * grid.addEventListener('cell-commit', (e) => {\n * if (e.detail.field === 'quantity') {\n * e.detail.updateRow({ total: e.detail.row.price * e.detail.value });\n * }\n * });\n * ```\n */\n updateRow: (changes: Partial<TRow>) => void;\n /**\n * Register a callback invoked when the cell's underlying value changes\n * while the editor is open (e.g., via `updateRow()` from another cell's commit).\n *\n * Built-in editors auto-update their input values. Custom/framework editors\n * should use this to stay in sync with external mutations.\n *\n * @example\n * ```typescript\n * const editor = (ctx: ColumnEditorContext) => {\n * const input = document.createElement('input');\n * input.value = String(ctx.value);\n * ctx.onValueChange?.((newValue) => {\n * input.value = String(newValue);\n * });\n * return input;\n * };\n * ```\n */\n onValueChange?: (callback: (newValue: TValue) => void) => void;\n}\n// #endregion\n\n// #region Renderer Types\n/**\n * Context passed to custom view renderers (pure display – no commit helpers).\n *\n * Used by `viewRenderer` and `renderer` column properties to create\n * custom cell content. Return a DOM node or HTML string.\n *\n * @example\n * ```typescript\n * // Status badge renderer\n * const statusRenderer: ColumnViewRenderer = (ctx: CellRenderContext) => {\n * const badge = document.createElement('span');\n * badge.className = `badge badge-${ctx.value}`;\n * badge.textContent = ctx.value;\n * return badge;\n * };\n *\n * // Progress bar using row data\n * const progressRenderer: ColumnViewRenderer = (ctx) => {\n * const bar = document.createElement('div');\n * bar.className = 'progress-bar';\n * bar.style.width = `${ctx.value}%`;\n * bar.title = `${ctx.row.taskName}: ${ctx.value}%`;\n * return bar;\n * };\n *\n * // Return HTML string (simpler, less performant)\n * const htmlRenderer: ColumnViewRenderer = (ctx) => {\n * return `<strong>${ctx.value}</strong>`;\n * };\n * ```\n *\n * @see {@link ColumnViewRenderer} for the renderer function signature\n */\nexport interface CellRenderContext<TRow = any, TValue = any> {\n /** Row object for the cell being rendered. */\n row: TRow;\n /** Value at field. */\n value: TValue;\n /** Field key. */\n field: keyof TRow & string;\n /** Column configuration reference. */\n column: ColumnConfig<TRow>;\n /**\n * The cell DOM element being rendered into.\n * Framework adapters can use this to cache per-cell state (e.g., React roots).\n * @internal\n */\n cellEl?: HTMLElement;\n}\n\n/**\n * Custom view renderer function for cell content.\n *\n * Returns one of:\n * - `Node` - DOM element to display in the cell\n * - `string` - HTML string (parsed and inserted)\n * - `void | null` - Use default text rendering\n *\n * @example\n * ```typescript\n * // DOM element (recommended for interactivity)\n * const avatarRenderer: ColumnViewRenderer<Employee> = (ctx) => {\n * const img = document.createElement('img');\n * img.src = ctx.row.avatarUrl;\n * img.alt = ctx.row.name;\n * img.className = 'avatar';\n * return img;\n * };\n *\n * // HTML string (simpler, good for static content)\n * const emailRenderer: ColumnViewRenderer = (ctx) => {\n * return `<a href=\"mailto:${ctx.value}\">${ctx.value}</a>`;\n * };\n *\n * // Conditional rendering\n * const conditionalRenderer: ColumnViewRenderer = (ctx) => {\n * if (!ctx.value) return null; // Use default\n * return `<em>${ctx.value}</em>`;\n * };\n * ```\n *\n * @see {@link CellRenderContext} for available context properties\n */\nexport type ColumnViewRenderer<TRow = unknown, TValue = unknown> = (\n ctx: CellRenderContext<TRow, TValue>,\n) => Node | string | void | null;\n// #endregion\n\n// #region Header Renderer Types\n/**\n * Context passed to `headerLabelRenderer` for customizing header label content.\n * The framework handles sort icons, filter buttons, resize handles, and click interactions.\n *\n * @example\n * ```typescript\n * headerLabelRenderer: (ctx) => {\n * const span = document.createElement('span');\n * span.innerHTML = `${ctx.value} <span class=\"required\">*</span>`;\n * return span;\n * }\n * ```\n */\nexport interface HeaderLabelContext<TRow = unknown> {\n /** Column configuration reference. */\n column: ColumnConfig<TRow>;\n /** The header text (from column.header or column.field). */\n value: string;\n}\n\n/**\n * Context passed to `headerRenderer` for complete control over header cell content.\n * When using this, you control the header content. Resize handles are added automatically\n * for resizable columns.\n *\n * @example\n * ```typescript\n * headerRenderer: (ctx) => {\n * const div = document.createElement('div');\n * div.className = 'custom-header';\n * div.innerHTML = `<span>${ctx.value}</span>`;\n * // Optionally include sort icon\n * const sortIcon = ctx.renderSortIcon();\n * if (sortIcon) div.appendChild(sortIcon);\n * return div;\n * }\n * ```\n */\nexport interface HeaderCellContext<TRow = unknown> {\n /** Column configuration reference. */\n column: ColumnConfig<TRow>;\n /** The header text (from column.header or column.field). */\n value: string;\n /** Current sort state for this column. */\n sortState: 'asc' | 'desc' | null;\n /** Whether the column has an active filter. */\n filterActive: boolean;\n /** The header cell DOM element being rendered into. */\n cellEl: HTMLElement;\n /**\n * Render the standard sort indicator icon.\n * Returns null if column is not sortable.\n */\n renderSortIcon: () => HTMLElement | null;\n /**\n * Render the standard filter button.\n * Returns null if FilteringPlugin is not active or column is not filterable.\n * Note: The actual button is added by FilteringPlugin's afterRender hook.\n */\n renderFilterButton: () => HTMLElement | null;\n}\n\n/**\n * Header label renderer function type.\n * Customize the label while framework handles sort icons, filter buttons, resize handles.\n *\n * Use this for simple label customizations without taking over the entire header.\n * The grid automatically appends sort icons, filter buttons, and resize handles.\n *\n * @example\n * ```typescript\n * // Add required indicator\n * const requiredHeader: HeaderLabelRenderer = (ctx) => {\n * return `${ctx.value} <span style=\"color: red;\">*</span>`;\n * };\n *\n * // Add unit suffix\n * const priceHeader: HeaderLabelRenderer = (ctx) => {\n * const span = document.createElement('span');\n * span.innerHTML = `${ctx.value} <small>(USD)</small>`;\n * return span;\n * };\n *\n * // Column config usage\n * columns: [\n * { field: 'name', headerLabelRenderer: requiredHeader },\n * { field: 'price', headerLabelRenderer: priceHeader },\n * ]\n * ```\n *\n * @see {@link HeaderLabelContext} for context properties\n * @see {@link HeaderRenderer} for full header control\n */\nexport type HeaderLabelRenderer<TRow = unknown> = (ctx: HeaderLabelContext<TRow>) => Node | string | void | null;\n\n/**\n * Header cell renderer function type.\n * Full control over header cell content. User is responsible for all content and interactions.\n *\n * When using this, you have complete control but must manually include\n * sort icons, filter buttons, and resize handles using the helper functions.\n *\n * @example\n * ```typescript\n * // Custom header with all standard elements\n * const customHeader: HeaderRenderer = (ctx) => {\n * const div = document.createElement('div');\n * div.className = 'custom-header';\n * div.innerHTML = `<span class=\"label\">${ctx.value}</span>`;\n *\n * // Add sort icon (returns null if not sortable)\n * const sortIcon = ctx.renderSortIcon();\n * if (sortIcon) div.appendChild(sortIcon);\n *\n * // Add filter button (returns null if not filterable)\n * const filterBtn = ctx.renderFilterButton();\n * if (filterBtn) div.appendChild(filterBtn);\n *\n * // Resize handles are added automatically for resizable columns\n * return div;\n * };\n *\n * // Minimal header (no sort/resize)\n * const minimalHeader: HeaderRenderer = (ctx) => {\n * return `<div class=\"minimal\">${ctx.value}</div>`;\n * };\n *\n * // Column config usage\n * columns: [\n * { field: 'name', headerRenderer: customHeader },\n * ]\n * ```\n *\n * @see {@link HeaderCellContext} for context properties and helper functions\n * @see {@link HeaderLabelRenderer} for simpler label-only customization\n */\nexport type HeaderRenderer<TRow = unknown> = (ctx: HeaderCellContext<TRow>) => Node | string | void | null;\n// #endregion\n\n// #region Framework Adapter Interface\n/**\n * Framework adapter interface for handling framework-specific component instantiation.\n * Allows framework libraries (Angular, React, Vue) to register handlers that convert\n * declarative light DOM elements into functional renderers/editors.\n *\n * @example\n * ```typescript\n * // In @toolbox-web/grid-angular\n * class AngularGridAdapter implements FrameworkAdapter {\n * canHandle(element: HTMLElement): boolean {\n * return element.tagName.startsWith('APP-');\n * }\n * createRenderer(element: HTMLElement): ColumnViewRenderer {\n * return (ctx) => {\n * // Angular-specific instantiation logic\n * const componentRef = createComponent(...);\n * componentRef.setInput('value', ctx.value);\n * return componentRef.location.nativeElement;\n * };\n * }\n * createEditor(element: HTMLElement): ColumnEditorSpec {\n * return (ctx) => {\n * // Angular-specific editor with commit/cancel\n * const componentRef = createComponent(...);\n * componentRef.setInput('value', ctx.value);\n * // Subscribe to commit/cancel outputs\n * return componentRef.location.nativeElement;\n * };\n * }\n * }\n *\n * // User registers adapter once in their app\n * GridElement.registerAdapter(new AngularGridAdapter(injector, appRef));\n * ```\n * @category Framework Adapters\n */\nexport interface FrameworkAdapter {\n /**\n * Determines if this adapter can handle the given element.\n * Typically checks tag name, attributes, or other conventions.\n */\n canHandle(element: HTMLElement): boolean;\n\n /**\n * Creates a view renderer function from a light DOM element.\n * The renderer receives cell context and returns DOM or string.\n */\n createRenderer<TRow = unknown, TValue = unknown>(element: HTMLElement): ColumnViewRenderer<TRow, TValue>;\n\n /**\n * Creates an editor spec from a light DOM element.\n * The editor receives context with commit/cancel and returns DOM.\n * Returns undefined if no editor template is registered, allowing the grid\n * to use its default built-in editors.\n */\n createEditor<TRow = unknown, TValue = unknown>(element: HTMLElement): ColumnEditorSpec<TRow, TValue> | undefined;\n\n /**\n * Creates a tool panel renderer from a light DOM element.\n * The renderer receives a container element and optionally returns a cleanup function.\n */\n createToolPanelRenderer?(element: HTMLElement): ((container: HTMLElement) => void | (() => void)) | undefined;\n\n /**\n * Gets type-level defaults from an application-level registry.\n * Used by Angular's `GridTypeRegistry` and React's `GridTypeProvider`.\n *\n * @param type - The column type (e.g., 'date', 'currency', 'country')\n * @returns Type defaults for renderer/editor, or undefined if not registered\n */\n getTypeDefault?<TRow = unknown>(type: string): TypeDefault<TRow> | undefined;\n\n /**\n * Called when a cell's content is about to be wiped (e.g., when exiting edit mode,\n * scroll-recycling a row, or rebuilding a row).\n *\n * Framework adapters should use this to properly destroy cached views/components\n * associated with the cell to prevent memory leaks.\n *\n * @param cellEl - The cell element whose content is being released\n */\n releaseCell?(cellEl: HTMLElement): void;\n}\n// #endregion\n\n// #region Internal Types\n\n/**\n * Column internal properties used during light DOM parsing.\n * Stores attribute-based names before they're resolved to actual functions.\n * @internal\n */\nexport interface ColumnParsedAttributes {\n /** Editor name from `editor` attribute (resolved later) */\n __editorName?: string;\n /** Renderer name from `renderer` attribute (resolved later) */\n __rendererName?: string;\n}\n\n/**\n * Extended column config used internally.\n * Includes all internal properties needed during grid lifecycle.\n *\n * Plugin developers may need to access these when working with\n * column caching and compiled templates.\n *\n * @example\n * ```typescript\n * import type { ColumnInternal } from '@toolbox-web/grid';\n *\n * class MyPlugin extends BaseGridPlugin {\n * afterRender(): void {\n * // Access internal column properties\n * const columns = this.columns as ColumnInternal[];\n * for (const col of columns) {\n * // Check if column was auto-sized\n * if (col.__autoSized) {\n * console.log(`${col.field} was auto-sized`);\n * }\n * }\n * }\n * }\n * ```\n *\n * @see {@link ColumnConfig} for public column properties\n * @category Plugin Development\n * @internal\n */\nexport interface ColumnInternal<T = any> extends ColumnConfig<T>, ColumnParsedAttributes {\n __autoSized?: boolean;\n __userResized?: boolean;\n __renderedWidth?: number;\n /** Original configured width (for reset on double-click) */\n __originalWidth?: number;\n __viewTemplate?: HTMLElement;\n __editorTemplate?: HTMLElement;\n __headerTemplate?: HTMLElement;\n __compiledView?: CompiledViewFunction<T>;\n __compiledEditor?: (ctx: EditorExecContext<T>) => string;\n}\n\n/**\n * Row element with internal tracking properties.\n * Used during virtualization and row pooling.\n *\n * @category Plugin Development\n * @internal\n */\nexport interface RowElementInternal extends HTMLElement {\n /** Epoch marker for row render invalidation */\n __epoch?: number;\n /** Reference to the row data object for change detection */\n __rowDataRef?: unknown;\n /** Count of cells currently in edit mode */\n __editingCellCount?: number;\n /** Flag indicating this is a custom-rendered row (group row, etc.) */\n __isCustomRow?: boolean;\n}\n\n/**\n * Type-safe access to element.part API (DOMTokenList-like).\n * Used for CSS ::part styling support.\n * @internal\n */\nexport interface ElementWithPart {\n part?: DOMTokenList;\n}\n\n/**\n * Compiled view function type with optional blocked flag.\n * The __blocked flag is set when a template contains unsafe expressions.\n *\n * @category Plugin Development\n * @internal\n */\nexport interface CompiledViewFunction<T = any> {\n (ctx: CellContext<T>): string;\n /** Set to true when template was blocked due to unsafe expressions */\n __blocked?: boolean;\n}\n\n/**\n * Runtime cell context used internally for compiled template execution.\n *\n * Contains the minimal context needed to render a cell: the row data,\n * cell value, field name, and column configuration.\n *\n * @example\n * ```typescript\n * import type { CellContext, ColumnInternal } from '@toolbox-web/grid';\n *\n * // Used internally by compiled templates\n * const renderCell = (ctx: CellContext) => {\n * return `<span title=\"${ctx.field}\">${ctx.value}</span>`;\n * };\n * ```\n *\n * @see {@link CellRenderContext} for public cell render context\n * @see {@link EditorExecContext} for editor context with commit/cancel\n * @category Plugin Development\n */\nexport interface CellContext<T = any> {\n row: T;\n value: unknown;\n field: string;\n column: ColumnInternal<T>;\n}\n\n/**\n * Internal editor execution context extending the generic cell context with commit helpers.\n *\n * Used internally by the editing system. For public editor APIs,\n * prefer using {@link ColumnEditorContext}.\n *\n * @example\n * ```typescript\n * import type { EditorExecContext } from '@toolbox-web/grid';\n *\n * // Internal editor template execution\n * const execEditor = (ctx: EditorExecContext) => {\n * const input = document.createElement('input');\n * input.value = String(ctx.value);\n * input.onkeydown = (e) => {\n * if (e.key === 'Enter') ctx.commit(input.value);\n * if (e.key === 'Escape') ctx.cancel();\n * };\n * return input;\n * };\n * ```\n *\n * @see {@link ColumnEditorContext} for public editor context\n * @see {@link CellContext} for base cell context\n * @category Plugin Development\n */\nexport interface EditorExecContext<T = any> extends CellContext<T> {\n commit: (newValue: unknown) => void;\n cancel: () => void;\n}\n\n/**\n * Controller managing drag-based column resize lifecycle.\n *\n * Exposed internally for plugins that need to interact with resize behavior.\n *\n * @example\n * ```typescript\n * import type { ResizeController, InternalGrid } from '@toolbox-web/grid';\n *\n * class MyPlugin extends BaseGridPlugin {\n * handleColumnAction(colIndex: number): void {\n * const grid = this.grid as InternalGrid;\n * const resizeCtrl = grid._resizeController;\n *\n * // Check if resize is in progress\n * if (resizeCtrl?.isResizing) {\n * return; // Don't interfere\n * }\n *\n * // Reset column to configured width\n * resizeCtrl?.resetColumn(colIndex);\n * }\n * }\n * ```\n *\n * @see {@link ColumnResizeDetail} for resize event details\n * @category Plugin Development\n */\nexport interface ResizeController {\n start: (e: MouseEvent, colIndex: number, cell: HTMLElement) => void;\n /** Reset a column to its configured width (or auto-size if none configured). */\n resetColumn: (colIndex: number) => void;\n dispose: () => void;\n /** True while a resize drag is in progress (used to suppress header click/sort). */\n isResizing: boolean;\n}\n\n/**\n * Virtual window bookkeeping; modified in-place as scroll position changes.\n *\n * Tracks virtualization state for row rendering. The grid only renders\n * rows within the visible viewport window (start to end) plus overscan.\n *\n * @example\n * ```typescript\n * import type { VirtualState, InternalGrid } from '@toolbox-web/grid';\n *\n * class MyPlugin extends BaseGridPlugin {\n * logVirtualWindow(): void {\n * const grid = this.grid as InternalGrid;\n * const vs = grid.virtualization;\n *\n * console.log(`Row height: ${vs.rowHeight}px`);\n * console.log(`Visible rows: ${vs.start} to ${vs.end}`);\n * console.log(`Virtualization: ${vs.enabled ? 'on' : 'off'}`);\n * }\n * }\n * ```\n *\n * @see {@link GridConfig.rowHeight} for configuring row height\n * @category Plugin Development\n */\nexport interface VirtualState {\n enabled: boolean;\n rowHeight: number;\n /** Threshold for bypassing virtualization (renders all rows if totalRows <= bypassThreshold) */\n bypassThreshold: number;\n start: number;\n end: number;\n /** Faux scrollbar element that provides scroll events (AG Grid pattern) */\n container: HTMLElement | null;\n /** Rows viewport element for measuring visible area height */\n viewportEl: HTMLElement | null;\n /** Spacer element inside faux scrollbar for setting virtual height */\n totalHeightEl: HTMLElement | null;\n\n // --- Variable Row Height Support (Phase 1) ---\n\n /**\n * Position cache for variable row heights.\n * Index-based array mapping row index → {offset, height, measured}.\n * Rebuilt when row count changes (expand/collapse, filter).\n * `null` when using uniform row heights (default).\n */\n positionCache: RowPositionEntry[] | null;\n\n /**\n * Height cache by row identity.\n * Persists row heights across position cache rebuilds.\n * Uses dual storage: Map for string keys (rowId, __rowCacheKey) and WeakMap for object refs.\n */\n heightCache: {\n /** Heights keyed by string (synthetic rows with __rowCacheKey, or rowId-keyed rows) */\n byKey: Map<string, number>;\n /** Heights keyed by object reference (data rows without rowId) */\n byRef: WeakMap<object, number>;\n };\n\n /** Running average of measured row heights. Used for estimating unmeasured rows. */\n averageHeight: number;\n\n /** Number of rows that have been measured. */\n measuredCount: number;\n\n /** Whether variable row height mode is active (rowHeight is a function). */\n variableHeights: boolean;\n\n // --- Cached Geometry (avoid forced layout reads on scroll hot path) ---\n\n /** Cached viewport element height. Updated by ResizeObserver and force-refresh only. */\n cachedViewportHeight: number;\n\n /** Cached faux scrollbar element height. Updated alongside viewport height. */\n cachedFauxHeight: number;\n\n /** Cached scroll-area element height. Updated alongside viewport/faux heights. */\n cachedScrollAreaHeight: number;\n\n /** Cached reference to .tbw-scroll-area element. Set during scroll listener setup. */\n scrollAreaEl: HTMLElement | null;\n}\n\n// RowElementInternal is now defined earlier in the file with all internal properties\n\n/**\n * Union type for input-like elements that have a `value` property.\n * Covers standard form elements and custom elements with value semantics.\n *\n * @category Plugin Development\n * @internal\n */\nexport type InputLikeElement = HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement | { value: unknown };\n// #endregion\n\n// #region Grouping & Footer Public Types\n/**\n * Group row rendering customization options.\n * Controls how group header rows are displayed in the GroupingRowsPlugin.\n *\n * @example\n * ```typescript\n * import { GroupingRowsPlugin } from '@toolbox-web/grid/all';\n *\n * new GroupingRowsPlugin({\n * groupBy: ['department', 'team'],\n * render: {\n * // Group row spans all columns\n * fullWidth: true,\n *\n * // Custom label format\n * formatLabel: (value, depth, key) => {\n * if (depth === 0) return `Department: ${value}`;\n * return `Team: ${value}`;\n * },\n *\n * // Show aggregates in group rows (when not fullWidth)\n * aggregators: {\n * salary: 'sum',\n * age: 'avg',\n * },\n *\n * // Custom CSS class\n * class: 'my-group-row',\n * },\n * });\n * ```\n *\n * @see {@link AggregatorRef} for aggregation options\n */\nexport interface RowGroupRenderConfig {\n /** If true, group rows span all columns (single full-width cell). Default false. */\n fullWidth?: boolean;\n /** Optional label formatter override. Receives raw group value + depth. */\n formatLabel?: (value: unknown, depth: number, key: string) => string;\n /** Optional aggregate overrides per field for group summary cells (only when not fullWidth). */\n aggregators?: Record<string, AggregatorRef>;\n /** Additional CSS class applied to each group row root element. */\n class?: string;\n}\n\n/**\n * Reference to an aggregation function for footer/group summaries.\n *\n * Can be either:\n * - A built-in aggregator name: `'sum'`, `'avg'`, `'min'`, `'max'`, `'count'`\n * - A custom function that calculates the aggregate value\n *\n * @example\n * ```typescript\n * // Built-in aggregator\n * { field: 'amount', aggregator: 'sum' }\n *\n * // Custom aggregator function\n * { field: 'price', aggregator: (rows, field) => {\n * const values = rows.map(r => r[field]).filter(v => v != null);\n * return values.length ? Math.max(...values) : null;\n * }}\n * ```\n *\n * @see {@link RowGroupRenderConfig} for using aggregators in group rows\n */\nexport type AggregatorRef = string | ((rows: unknown[], field: string, column?: unknown) => unknown);\n\n/**\n * Result of automatic column inference from sample rows.\n *\n * When no columns are configured, the grid analyzes the first row of data\n * to automatically generate column definitions with inferred types.\n *\n * @example\n * ```typescript\n * // Automatic inference (no columns configured)\n * grid.rows = [\n * { name: 'Alice', age: 30, active: true, hireDate: new Date() },\n * ];\n * // Grid infers:\n * // - name: type 'string'\n * // - age: type 'number'\n * // - active: type 'boolean'\n * // - hireDate: type 'date'\n *\n * // Access inferred result programmatically\n * const config = await grid.getConfig();\n * console.log(config.columns); // Inferred columns\n * ```\n *\n * @see {@link ColumnConfig} for column configuration options\n * @see {@link ColumnType} for type inference rules\n */\nexport interface InferredColumnResult<TRow = unknown> {\n /** Generated column configurations based on data analysis */\n columns: ColumnConfigMap<TRow>;\n /** Map of field names to their inferred types */\n typeMap: Record<string, ColumnType>;\n}\n\n/**\n * Column sizing mode.\n *\n * - `'fixed'` - Columns use their configured widths. Horizontal scrolling if content overflows.\n * - `'stretch'` - Columns stretch proportionally to fill available width. No horizontal scrolling.\n *\n * @example\n * ```typescript\n * // Fixed widths - good for many columns\n * grid.fitMode = 'fixed';\n *\n * // Stretch to fill - good for few columns\n * grid.fitMode = 'stretch';\n *\n * // Via gridConfig\n * grid.gridConfig = { fitMode: 'stretch' };\n * ```\n */\nexport const FitModeEnum = {\n STRETCH: 'stretch',\n FIXED: 'fixed',\n} as const;\nexport type FitMode = (typeof FitModeEnum)[keyof typeof FitModeEnum]; // evaluates to 'stretch' | 'fixed'\n// #endregion\n\n// #region Plugin Interface\n/**\n * Minimal plugin interface for type-checking.\n * This interface is defined here to avoid circular imports with BaseGridPlugin.\n * All plugins must satisfy this shape (BaseGridPlugin implements it).\n *\n * @example\n * ```typescript\n * // Using plugins in grid config\n * import { SelectionPlugin, FilteringPlugin } from '@toolbox-web/grid/all';\n *\n * grid.gridConfig = {\n * plugins: [\n * new SelectionPlugin({ mode: 'row' }),\n * new FilteringPlugin({ debounceMs: 200 }),\n * ],\n * };\n *\n * // Accessing plugin instance at runtime\n * const selection = grid.getPlugin(SelectionPlugin);\n * if (selection) {\n * selection.selectAll();\n * }\n * ```\n *\n * @category Plugin Development\n */\nexport interface GridPlugin {\n /** Unique plugin identifier */\n readonly name: string;\n /** Plugin version */\n readonly version: string;\n /** CSS styles to inject into the grid */\n readonly styles?: string;\n}\n// #endregion\n\n// #region Grid Config\n/**\n * Grid configuration object - the **single source of truth** for grid behavior.\n *\n * Users can configure the grid via multiple input methods, all of which converge\n * into an effective `GridConfig` internally:\n *\n * **Configuration Input Methods:**\n * - `gridConfig` property - direct assignment of this object\n * - `columns` property - shorthand for `gridConfig.columns`\n * - `fitMode` property - shorthand for `gridConfig.fitMode`\n * - Light DOM `<tbw-grid-column>` - declarative columns (merged into `columns`)\n * - Light DOM `<tbw-grid-header>` - declarative shell header (merged into `shell.header`)\n *\n * **Precedence (when same property set multiple ways):**\n * Individual props (`fitMode`) > `columns` prop > Light DOM > `gridConfig`\n *\n * @example\n * ```ts\n * // Via gridConfig (recommended for complex setups)\n * grid.gridConfig = {\n * columns: [{ field: 'name' }, { field: 'age' }],\n * fitMode: 'stretch',\n * plugins: [new SelectionPlugin()],\n * shell: { header: { title: 'My Grid' } }\n * };\n *\n * // Via individual props (convenience for simple cases)\n * grid.columns = [{ field: 'name' }, { field: 'age' }];\n * grid.fitMode = 'stretch';\n * ```\n */\nexport interface GridConfig<TRow = any> {\n /**\n * Column definitions. Can also be set via `columns` prop or `<tbw-grid-column>` light DOM.\n * @see {@link ColumnConfig} for column options\n * @see {@link ColumnConfigMap}\n */\n columns?: ColumnConfigMap<TRow>;\n /**\n * Dynamic CSS class(es) for data rows.\n * Called for each row during rendering. Return class names to add to the row element.\n *\n * @example\n * ```typescript\n * // Highlight inactive rows\n * rowClass: (row) => row.active ? [] : ['inactive', 'dimmed']\n *\n * // Status-based row styling\n * rowClass: (row) => [`priority-${row.priority}`]\n * ```\n */\n rowClass?: (row: TRow) => string[];\n /** Sizing mode for columns. Can also be set via `fitMode` prop. */\n fitMode?: FitMode;\n\n /**\n * Grid-wide sorting toggle.\n * When false, disables sorting for all columns regardless of their individual `sortable` setting.\n * When true (default), columns with `sortable: true` can be sorted.\n *\n * This affects:\n * - Header click handlers for sorting\n * - Sort indicator visibility\n * - Multi-sort plugin behavior (if loaded)\n *\n * @default true\n *\n * @example\n * ```typescript\n * // Disable all sorting\n * gridConfig = { sortable: false };\n *\n * // Enable sorting (default) - individual columns still need sortable: true\n * gridConfig = { sortable: true };\n * ```\n */\n sortable?: boolean;\n\n /**\n * Grid-wide resizing toggle.\n * When false, disables column resizing for all columns regardless of their individual `resizable` setting.\n * When true (default), columns with `resizable: true` (or resizable not set, since it defaults to true) can be resized.\n *\n * This affects:\n * - Resize handle visibility in header cells\n * - Double-click to auto-size behavior\n *\n * @default true\n *\n * @example\n * ```typescript\n * // Disable all column resizing\n * gridConfig = { resizable: false };\n *\n * // Enable resizing (default) - individual columns can opt out with resizable: false\n * gridConfig = { resizable: true };\n * ```\n */\n resizable?: boolean;\n\n /**\n * Row height in pixels for virtualization calculations.\n * The virtualization system assumes uniform row heights for performance.\n *\n * If not specified, the grid measures the first rendered row's height,\n * which respects the CSS variable `--tbw-row-height` set by themes.\n *\n * Set this explicitly when:\n * - Row content may wrap to multiple lines (also set `--tbw-cell-white-space: normal`)\n * - Using custom row templates with variable content\n * - You want to override theme-defined row height\n * - Rows have different heights based on content (use function form)\n *\n * **Variable Row Heights**: When a function is provided, the grid enables variable height\n * virtualization. Heights are measured on first render and cached by row identity.\n *\n * @default Auto-measured from first row (respects --tbw-row-height CSS variable)\n *\n * @example\n * ```ts\n * // Fixed height for all rows\n * gridConfig = { rowHeight: 56 };\n *\n * // Variable height based on content\n * gridConfig = {\n * rowHeight: (row, index) => row.hasDetails ? 80 : 40,\n * };\n *\n * // Return undefined to trigger DOM auto-measurement\n * gridConfig = {\n * rowHeight: (row) => row.isExpanded ? undefined : 40,\n * };\n * ```\n */\n rowHeight?: number | ((row: TRow, index: number) => number | undefined);\n /**\n * Array of plugin instances.\n * Each plugin is instantiated with its configuration and attached to this grid.\n *\n * @example\n * ```ts\n * plugins: [\n * new SelectionPlugin({ mode: 'range' }),\n * new MultiSortPlugin(),\n * new FilteringPlugin({ debounceMs: 150 }),\n * ]\n * ```\n */\n plugins?: GridPlugin[];\n\n /**\n * Saved column state to restore on initialization.\n * Includes order, width, visibility, sort, and plugin-contributed state.\n */\n columnState?: GridColumnState;\n\n /**\n * Shell configuration for header bar and tool panels.\n * When configured, adds an optional wrapper with title, toolbar, and collapsible side panels.\n */\n shell?: ShellConfig;\n\n /**\n * Grid-wide icon configuration.\n * Provides consistent icons across all plugins (tree, grouping, sorting, etc.).\n * Plugins will use these by default but can override with their own config.\n */\n icons?: GridIcons;\n\n /**\n * Grid-wide animation configuration.\n * Controls animations for expand/collapse, reordering, and other visual transitions.\n * Individual plugins can override these defaults in their own config.\n */\n animation?: AnimationConfig;\n\n /**\n * Custom sort handler for full control over sorting behavior.\n *\n * When provided, this handler is called instead of the built-in sorting logic.\n * Enables custom sorting algorithms, server-side sorting, or plugin-specific sorting.\n *\n * The handler receives:\n * - `rows`: Current row array to sort\n * - `sortState`: Sort field and direction (1 = asc, -1 = desc)\n * - `columns`: Column configurations (for accessing sortComparator)\n *\n * Return the sorted array (sync) or a Promise that resolves to the sorted array (async).\n * For server-side sorting, return a Promise that resolves when data is fetched.\n *\n * @example\n * ```ts\n * // Custom stable sort\n * sortHandler: (rows, state, cols) => {\n * return stableSort(rows, (a, b) => compare(a[state.field], b[state.field]) * state.direction);\n * }\n *\n * // Server-side sorting\n * sortHandler: async (rows, state) => {\n * const response = await fetch(`/api/data?sort=${state.field}&dir=${state.direction}`);\n * return response.json();\n * }\n * ```\n */\n sortHandler?: SortHandler<TRow>;\n\n /**\n * Function to extract a unique identifier from a row.\n * Used by `updateRow()`, `getRow()`, and ID-based tracking.\n *\n * If not provided, falls back to `row.id` or `row._id` if present.\n * Rows without IDs are silently skipped during map building.\n * Only throws when explicitly calling `getRowId()` or `updateRow()` on a row without an ID.\n *\n * @example\n * ```ts\n * // Simple field\n * getRowId: (row) => row.id\n *\n * // Composite key\n * getRowId: (row) => `${row.voyageId}-${row.legNumber}`\n *\n * // UUID field\n * getRowId: (row) => row.uuid\n * ```\n */\n getRowId?: (row: TRow) => string;\n\n /**\n * Type-level renderer and editor defaults.\n *\n * Keys can be:\n * - Built-in types: `'string'`, `'number'`, `'date'`, `'boolean'`, `'select'`\n * - Custom types: `'currency'`, `'country'`, `'status'`, etc.\n *\n * Resolution order (highest priority first):\n * 1. Column-level (`column.renderer` / `column.editor`)\n * 2. Grid-level (`gridConfig.typeDefaults[column.type]`)\n * 3. App-level (Angular `GridTypeRegistry`, React `GridTypeProvider`)\n * 4. Built-in (checkbox for boolean, select for select, etc.)\n * 5. Fallback (plain text / text input)\n *\n * @example\n * ```typescript\n * typeDefaults: {\n * date: { editor: myDatePickerEditor },\n * country: {\n * renderer: (ctx) => {\n * const span = document.createElement('span');\n * span.innerHTML = `<img src=\"/flags/${ctx.value}.svg\" /> ${ctx.value}`;\n * return span;\n * },\n * editor: (ctx) => createCountrySelect(ctx)\n * }\n * }\n * ```\n */\n typeDefaults?: Record<string, TypeDefault<TRow>>;\n\n // #region Accessibility\n\n /**\n * Accessible label for the grid.\n * Sets `aria-label` on the grid's internal table element for screen readers.\n *\n * If not provided and `shell.header.title` is set, the title is used automatically.\n *\n * @example\n * ```ts\n * gridConfig = { gridAriaLabel: 'Employee data' };\n * ```\n */\n gridAriaLabel?: string;\n\n /**\n * ID of an element that describes the grid.\n * Sets `aria-describedby` on the grid's internal table element.\n *\n * @example\n * ```html\n * <p id=\"grid-desc\">This table shows all active employees.</p>\n * <tbw-grid></tbw-grid>\n * ```\n * ```ts\n * gridConfig = { gridAriaDescribedBy: 'grid-desc' };\n * ```\n */\n gridAriaDescribedBy?: string;\n\n // #endregion\n\n // #region Loading\n\n /**\n * Custom renderer for the loading overlay.\n *\n * When provided, replaces the default spinner with custom content.\n * Receives a context object with the current loading size.\n *\n * @example\n * ```typescript\n * // Simple text loading indicator\n * loadingRenderer: () => {\n * const el = document.createElement('div');\n * el.textContent = 'Loading...';\n * return el;\n * }\n *\n * // Custom spinner component\n * loadingRenderer: (ctx) => {\n * const spinner = document.createElement('my-spinner');\n * spinner.size = ctx.size === 'large' ? 48 : 24;\n * return spinner;\n * }\n * ```\n */\n loadingRenderer?: LoadingRenderer;\n\n // #endregion\n}\n// #endregion\n\n// #region Animation\n\n/**\n * Sort state passed to custom sort handlers.\n * Represents the current sorting configuration for a column.\n *\n * @example\n * ```typescript\n * // In a custom sort handler\n * const sortHandler: SortHandler = (rows, sortState, columns) => {\n * const { field, direction } = sortState;\n * console.log(`Sorting by ${field} ${direction === 1 ? 'ASC' : 'DESC'}`);\n *\n * return [...rows].sort((a, b) => {\n * const aVal = a[field];\n * const bVal = b[field];\n * return (aVal < bVal ? -1 : aVal > bVal ? 1 : 0) * direction;\n * });\n * };\n * ```\n *\n * @see {@link SortHandler} for custom sort handler signature\n * @see {@link SortChangeDetail} for sort change events\n */\nexport interface SortState {\n /** Field to sort by */\n field: string;\n /** Sort direction: 1 = ascending, -1 = descending */\n direction: 1 | -1;\n}\n\n/**\n * Custom sort handler function signature.\n *\n * Enables full control over sorting behavior including server-side sorting,\n * custom algorithms, or multi-column sorting.\n *\n * @param rows - Current row array to sort\n * @param sortState - Sort field and direction\n * @param columns - Column configurations (for accessing sortComparator)\n * @returns Sorted array (sync) or Promise resolving to sorted array (async)\n *\n * @example\n * ```typescript\n * // Custom client-side sort with locale awareness\n * const localeSortHandler: SortHandler<Employee> = (rows, state, cols) => {\n * const col = cols.find(c => c.field === state.field);\n * return [...rows].sort((a, b) => {\n * const aVal = String(a[state.field] ?? '');\n * const bVal = String(b[state.field] ?? '');\n * return aVal.localeCompare(bVal) * state.direction;\n * });\n * };\n *\n * // Server-side sorting\n * const serverSortHandler: SortHandler<Employee> = async (rows, state) => {\n * const response = await fetch(\n * `/api/employees?sortBy=${state.field}&dir=${state.direction}`\n * );\n * return response.json();\n * };\n *\n * grid.gridConfig = {\n * sortHandler: localeSortHandler,\n * };\n * ```\n *\n * @see {@link SortState} for the sort state object\n * @see {@link GridConfig.sortHandler} for configuring the handler\n * @see {@link BaseColumnConfig.sortComparator} for column-level comparators\n */\nexport type SortHandler<TRow = any> = (\n rows: TRow[],\n sortState: SortState,\n columns: ColumnConfig<TRow>[],\n) => TRow[] | Promise<TRow[]>;\n\n// #region Loading\n\n/**\n * Loading indicator size variant.\n *\n * - `'large'`: 48x48px max - used for grid-level loading overlay (`grid.loading = true`)\n * - `'small'`: Follows row height - used for row/cell loading states\n *\n * @example\n * ```typescript\n * // Custom loading renderer adapting to size\n * const myLoader: LoadingRenderer = (ctx) => {\n * if (ctx.size === 'large') {\n * // Full overlay spinner\n * return '<div class=\"spinner-lg\"></div>';\n * }\n * // Inline row/cell spinner\n * return '<span class=\"spinner-sm\"></span>';\n * };\n * ```\n *\n * @see {@link LoadingRenderer} for custom loading renderer\n * @see {@link LoadingContext} for context passed to renderers\n */\nexport type LoadingSize = 'large' | 'small';\n\n/**\n * Context passed to custom loading renderers.\n *\n * Provides information about the loading indicator being rendered,\n * allowing the renderer to adapt its appearance based on the size variant.\n *\n * @example\n * ```typescript\n * const myLoadingRenderer: LoadingRenderer = (ctx: LoadingContext) => {\n * if (ctx.size === 'large') {\n * // Full-size spinner for grid-level loading\n * return '<div class=\"large-spinner\"></div>';\n * } else {\n * // Compact spinner for row/cell loading\n * return '<div class=\"small-spinner\"></div>';\n * }\n * };\n * ```\n *\n * @see {@link LoadingRenderer} for the renderer function signature\n * @see {@link LoadingSize} for available size variants\n */\nexport interface LoadingContext {\n /** The size variant being rendered: 'large' for grid-level, 'small' for row/cell */\n size: LoadingSize;\n}\n\n/**\n * Custom loading renderer function.\n * Returns an element or HTML string to display as the loading indicator.\n *\n * Used with the `loadingRenderer` property in {@link GridConfig} to replace\n * the default spinner with custom content.\n *\n * @param context - Context containing size information\n * @returns HTMLElement or HTML string\n *\n * @example\n * ```typescript\n * // Simple text loading indicator\n * const textLoader: LoadingRenderer = () => {\n * const el = document.createElement('div');\n * el.textContent = 'Loading...';\n * return el;\n * };\n *\n * // Custom spinner with size awareness\n * const customSpinner: LoadingRenderer = (ctx) => {\n * const spinner = document.createElement('my-spinner');\n * spinner.size = ctx.size === 'large' ? 48 : 24;\n * return spinner;\n * };\n *\n * // Material Design-style progress bar\n * const progressBar: LoadingRenderer = () => {\n * const container = document.createElement('div');\n * container.className = 'progress-bar-container';\n * container.innerHTML = '<div class=\"progress-bar\"></div>';\n * return container;\n * };\n *\n * grid.gridConfig = {\n * loadingRenderer: customSpinner,\n * };\n * ```\n *\n * @see {@link LoadingContext} for the context object passed to the renderer\n * @see {@link LoadingSize} for size variants ('large' | 'small')\n */\nexport type LoadingRenderer = (context: LoadingContext) => HTMLElement | string;\n\n// #endregion\n\n// #region Data Update Management\n\n/**\n * Indicates the origin of a data change.\n * Used to prevent infinite loops in cascade update handlers.\n *\n * - `'user'`: Direct user interaction via EditingPlugin (typing, selecting)\n * - `'cascade'`: Triggered by `updateRow()` in an event handler\n * - `'api'`: External programmatic update via `grid.updateRow()`\n *\n * @example\n * ```typescript\n * grid.addEventListener('cell-change', (e) => {\n * const { source, field, newValue } = e.detail;\n *\n * // Only cascade updates for user edits\n * if (source === 'user' && field === 'price') {\n * // Update calculated field (marked as 'cascade')\n * grid.updateRow(e.detail.rowId, {\n * total: newValue * e.detail.row.quantity,\n * });\n * }\n *\n * // Ignore cascade updates to prevent infinite loops\n * if (source === 'cascade') return;\n * });\n * ```\n *\n * @see {@link CellChangeDetail} for the event detail containing source\n * @category Data Management\n */\nexport type UpdateSource = 'user' | 'cascade' | 'api';\n\n/**\n * Detail for cell-change event (emitted by core after mutation).\n * This is an informational event that fires for ALL data mutations.\n *\n * Use this event for:\n * - Logging/auditing changes\n * - Cascading updates (updating other fields based on a change)\n * - Syncing changes to external state\n *\n * @example\n * ```typescript\n * grid.addEventListener('cell-change', (e: CustomEvent<CellChangeDetail>) => {\n * const { row, rowId, field, oldValue, newValue, source } = e.detail;\n *\n * console.log(`${field} changed from ${oldValue} to ${newValue}`);\n * console.log(`Change source: ${source}`);\n *\n * // Cascade: update total when price changes\n * if (source === 'user' && field === 'price') {\n * grid.updateRow(rowId, { total: newValue * row.quantity });\n * }\n * });\n * ```\n *\n * @see {@link UpdateSource} for understanding change origins\n * @see {@link CellCommitDetail} for the commit event (editing lifecycle)\n * @category Events\n */\nexport interface CellChangeDetail<TRow = unknown> {\n /** The row object (after mutation) */\n row: TRow;\n /** Stable row identifier */\n rowId: string;\n /** Current index in rows array */\n rowIndex: number;\n /** Field that changed */\n field: string;\n /** Value before change */\n oldValue: unknown;\n /** Value after change */\n newValue: unknown;\n /** All changes passed to updateRow/updateRows (for context) */\n changes: Partial<TRow>;\n /** Origin of this change */\n source: UpdateSource;\n}\n\n/**\n * Batch update specification for updateRows().\n *\n * Used when you need to update multiple rows at once efficiently.\n * The grid will batch all updates and trigger a single re-render.\n *\n * @example\n * ```typescript\n * // Update multiple rows in a single batch\n * const updates: RowUpdate<Employee>[] = [\n * { id: 'emp-1', changes: { status: 'active', updatedAt: new Date() } },\n * { id: 'emp-2', changes: { status: 'inactive' } },\n * { id: 'emp-3', changes: { salary: 75000 } },\n * ];\n *\n * grid.updateRows(updates);\n * ```\n *\n * @see {@link CellChangeDetail} for individual change events\n * @see {@link GridConfig.getRowId} for row identification\n * @category Data Management\n */\nexport interface RowUpdate<TRow = unknown> {\n /** Row identifier (from getRowId) */\n id: string;\n /** Fields to update */\n changes: Partial<TRow>;\n}\n\n// #endregion\n\n/**\n * Animation behavior mode.\n * - `true` or `'on'`: Animations always enabled\n * - `false` or `'off'`: Animations always disabled\n * - `'reduced-motion'`: Respects `prefers-reduced-motion` media query (default)\n *\n * @example\n * ```typescript\n * // Force animations on (ignore system preference)\n * grid.gridConfig = { animation: { mode: 'on' } };\n *\n * // Disable all animations\n * grid.gridConfig = { animation: { mode: false } };\n *\n * // Respect user's accessibility settings (default)\n * grid.gridConfig = { animation: { mode: 'reduced-motion' } };\n * ```\n *\n * @see {@link AnimationConfig} for full animation configuration\n */\nexport type AnimationMode = boolean | 'on' | 'off' | 'reduced-motion';\n\n/**\n * Animation style for visual transitions.\n * - `'slide'`: Slide/transform animation (e.g., expand down, slide left/right)\n * - `'fade'`: Opacity fade animation\n * - `'flip'`: FLIP technique for position changes (First, Last, Invert, Play)\n * - `false`: No animation for this specific feature\n *\n * @example\n * ```typescript\n * // Plugin-specific animation styles\n * new TreePlugin({\n * expandAnimation: 'slide', // Slide children down when expanding\n * });\n *\n * new ReorderPlugin({\n * animation: 'flip', // FLIP animation for column reordering\n * });\n * ```\n *\n * @see {@link AnimationConfig} for grid-wide animation settings\n * @see {@link ExpandCollapseAnimation} for expand/collapse-specific styles\n */\nexport type AnimationStyle = 'slide' | 'fade' | 'flip' | false;\n\n/**\n * Animation style for expand/collapse operations.\n * Subset of AnimationStyle - excludes 'flip' which is for position changes.\n * - `'slide'`: Slide down/up animation for expanding/collapsing content\n * - `'fade'`: Fade in/out animation\n * - `false`: No animation\n *\n * @example\n * ```typescript\n * // Tree rows slide down when expanding\n * new TreePlugin({ expandAnimation: 'slide' });\n *\n * // Row groups fade in/out\n * new GroupingRowsPlugin({ expandAnimation: 'fade' });\n *\n * // Master-detail panels with no animation\n * new MasterDetailPlugin({ expandAnimation: false });\n * ```\n *\n * @see {@link AnimationStyle} for all animation styles\n * @see {@link AnimationConfig} for grid-wide settings\n */\nexport type ExpandCollapseAnimation = 'slide' | 'fade' | false;\n\n/**\n * Type of row animation.\n * - `'change'`: Flash highlight when row data changes (e.g., after cell edit)\n * - `'insert'`: Slide-in animation for newly added rows\n * - `'remove'`: Fade-out animation for rows being removed\n *\n * @example\n * ```typescript\n * // Internal usage - row animation is triggered automatically:\n * // - 'change' after cell-commit event\n * // - 'insert' when rows are added to the grid\n * // - 'remove' when rows are deleted\n *\n * // The animation respects AnimationConfig.mode\n * grid.gridConfig = {\n * animation: { mode: 'on', duration: 300 },\n * };\n * ```\n *\n * @see {@link AnimationConfig} for animation configuration\n */\nexport type RowAnimationType = 'change' | 'insert' | 'remove';\n\n/**\n * Grid-wide animation configuration.\n * Controls global animation behavior - individual plugins define their own animation styles.\n * Duration and easing values set corresponding CSS variables on the grid element.\n *\n * @example\n * ```typescript\n * // Enable animations regardless of system preferences\n * grid.gridConfig = {\n * animation: {\n * mode: 'on',\n * duration: 300,\n * easing: 'cubic-bezier(0.4, 0, 0.2, 1)',\n * },\n * };\n *\n * // Disable all animations\n * grid.gridConfig = {\n * animation: { mode: 'off' },\n * };\n *\n * // Respect user's reduced-motion preference (default)\n * grid.gridConfig = {\n * animation: { mode: 'reduced-motion' },\n * };\n * ```\n *\n * @see {@link AnimationMode} for mode options\n */\nexport interface AnimationConfig {\n /**\n * Global animation mode.\n * @default 'reduced-motion'\n */\n mode?: AnimationMode;\n\n /**\n * Default animation duration in milliseconds.\n * Sets `--tbw-animation-duration` CSS variable.\n * @default 200\n */\n duration?: number;\n\n /**\n * Default easing function.\n * Sets `--tbw-animation-easing` CSS variable.\n * @default 'ease-out'\n */\n easing?: string;\n}\n\n/** Default animation configuration */\nexport const DEFAULT_ANIMATION_CONFIG: Required<Omit<AnimationConfig, 'sort'>> = {\n mode: 'reduced-motion',\n duration: 200,\n easing: 'ease-out',\n};\n\n// #endregion\n\n// #region Grid Icons\n\n/** Icon value - can be a string (text/HTML) or HTMLElement */\nexport type IconValue = string | HTMLElement;\n\n/**\n * Grid-wide icon configuration.\n * All icons are optional - sensible defaults are used when not specified.\n *\n * Icons can be text (including emoji), HTML strings (for SVG), or HTMLElement instances.\n *\n * @example\n * ```typescript\n * grid.gridConfig = {\n * icons: {\n * // Emoji icons\n * expand: '➕',\n * collapse: '➖',\n *\n * // Custom SVG icon\n * sortAsc: '<svg viewBox=\"0 0 16 16\"><path d=\"M8 4l4 8H4z\"/></svg>',\n *\n * // Font icon class (wrap in span)\n * filter: '<span class=\"icon icon-filter\"></span>',\n * },\n * };\n * ```\n *\n * @see {@link IconValue} for allowed icon formats\n */\nexport interface GridIcons {\n /** Expand icon for collapsed items (trees, groups, details). Default: '▶' */\n expand?: IconValue;\n /** Collapse icon for expanded items (trees, groups, details). Default: '▼' */\n collapse?: IconValue;\n /** Sort ascending indicator. Default: '▲' */\n sortAsc?: IconValue;\n /** Sort descending indicator. Default: '▼' */\n sortDesc?: IconValue;\n /** Sort neutral/unsorted indicator. Default: '⇅' */\n sortNone?: IconValue;\n /** Submenu arrow for context menus. Default: '▶' */\n submenuArrow?: IconValue;\n /** Drag handle icon for reordering. Default: '⋮⋮' */\n dragHandle?: IconValue;\n /** Tool panel toggle icon in toolbar. Default: '☰' */\n toolPanel?: IconValue;\n /** Filter icon in column headers. Default: SVG funnel icon */\n filter?: IconValue;\n /** Filter icon when filter is active. Default: same as filter with accent color */\n filterActive?: IconValue;\n /** Print icon for print button. Default: '🖨️' */\n print?: IconValue;\n}\n\n/** Default filter icon SVG */\nconst DEFAULT_FILTER_ICON =\n '<svg viewBox=\"0 0 16 16\" width=\"12\" height=\"12\"><path fill=\"currentColor\" d=\"M6 10.5a.5.5 0 0 1 .5-.5h3a.5.5 0 0 1 0 1h-3a.5.5 0 0 1-.5-.5zm-2-3a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7a.5.5 0 0 1-.5-.5zm-2-3a.5.5 0 0 1 .5-.5h11a.5.5 0 0 1 0 1h-11a.5.5 0 0 1-.5-.5z\"/></svg>';\n\n/** Default icons used when not overridden */\nexport const DEFAULT_GRID_ICONS: Required<GridIcons> = {\n expand: '▶',\n collapse: '▼',\n sortAsc: '▲',\n sortDesc: '▼',\n sortNone: '⇅',\n submenuArrow: '▶',\n dragHandle: '⋮⋮',\n toolPanel: '☰',\n filter: DEFAULT_FILTER_ICON,\n filterActive: DEFAULT_FILTER_ICON,\n print: '🖨️',\n};\n// #endregion\n\n// #region Shell Configuration\n\n/**\n * Shell configuration for the grid's optional header bar and tool panels.\n *\n * The shell provides a wrapper around the grid with:\n * - Header bar with title, toolbar buttons, and custom content\n * - Collapsible side panel for filters, column visibility, settings, etc.\n *\n * @example\n * ```typescript\n * grid.gridConfig = {\n * shell: {\n * header: {\n * title: 'Employee Directory',\n * },\n * toolPanel: {\n * position: 'right',\n * defaultOpen: 'columns', // Open by default\n * },\n * },\n * plugins: [new VisibilityPlugin()], // Adds \"Columns\" panel\n * };\n *\n * // Register custom tool panels\n * grid.registerToolPanel({\n * id: 'filters',\n * title: 'Filters',\n * icon: '🔍',\n * render: (container) => {\n * container.innerHTML = '<div>Filter controls...</div>';\n * },\n * });\n * ```\n *\n * @see {@link ShellHeaderConfig} for header options\n * @see {@link ToolPanelConfig} for tool panel options\n */\nexport interface ShellConfig {\n /** Shell header bar configuration */\n header?: ShellHeaderConfig;\n /** Tool panel configuration */\n toolPanel?: ToolPanelConfig;\n /**\n * Registered tool panels (from plugins, API, or Light DOM).\n * These are the actual panel definitions that can be opened.\n * @internal Set by ConfigManager during merge\n */\n toolPanels?: ToolPanelDefinition[];\n /**\n * Registered header content sections (from plugins or API).\n * Content rendered in the center of the shell header.\n * @internal Set by ConfigManager during merge\n */\n headerContents?: HeaderContentDefinition[];\n}\n\n/**\n * Shell header bar configuration\n */\nexport interface ShellHeaderConfig {\n /** Grid title displayed on the left (optional) */\n title?: string;\n /** Custom toolbar content (rendered before tool panel toggle) */\n toolbarContents?: ToolbarContentDefinition[];\n /**\n * Light DOM header content elements (parsed from <tbw-grid-header> children).\n * @internal Set by ConfigManager during merge\n */\n lightDomContent?: HTMLElement[];\n /**\n * Whether a tool buttons container was found in light DOM.\n * @internal Set by ConfigManager during merge\n */\n hasToolButtonsContainer?: boolean;\n}\n\n/**\n * Tool panel configuration\n */\nexport interface ToolPanelConfig {\n /** Panel position: 'left' | 'right' (default: 'right') */\n position?: 'left' | 'right';\n /** Default panel width in pixels (default: 280) */\n width?: number;\n /** Panel ID to open by default on load */\n defaultOpen?: string;\n /** Whether to persist open/closed state (requires Column State Events) */\n persistState?: boolean;\n /**\n * Close the tool panel when clicking outside of it.\n * When `true`, clicking anywhere outside the tool panel (but inside the grid)\n * will close the panel automatically.\n * @default false\n */\n closeOnClickOutside?: boolean;\n}\n\n/**\n * Toolbar content definition for the shell header toolbar area.\n * Register via `registerToolbarContent()` or use light DOM `<tbw-grid-tool-buttons>`.\n *\n * @example\n * ```typescript\n * grid.registerToolbarContent({\n * id: 'my-toolbar',\n * order: 10,\n * render: (container) => {\n * const btn = document.createElement('button');\n * btn.textContent = 'Refresh';\n * btn.onclick = () => console.log('clicked');\n * container.appendChild(btn);\n * return () => btn.remove();\n * },\n * });\n * ```\n */\nexport interface ToolbarContentDefinition {\n /** Unique content ID */\n id: string;\n /** Content factory - called once when shell header renders */\n render: (container: HTMLElement) => void | (() => void);\n /** Called when content is removed (for cleanup) */\n onDestroy?: () => void;\n /** Order priority (lower = first, default: 100) */\n order?: number;\n}\n\n/**\n * Tool panel definition registered by plugins or consumers.\n *\n * Register via `grid.registerToolPanel()` to add panels to the sidebar.\n * Panels appear as collapsible sections with icons and titles.\n *\n * @example\n * ```typescript\n * grid.registerToolPanel({\n * id: 'filters',\n * title: 'Filters',\n * icon: '🔍',\n * tooltip: 'Filter grid data',\n * order: 10, // Lower = appears first\n * render: (container) => {\n * container.innerHTML = `\n * <div class=\"filter-panel\">\n * <input type=\"text\" placeholder=\"Search...\" />\n * </div>\n * `;\n * // Return cleanup function\n * return () => container.innerHTML = '';\n * },\n * onClose: () => {\n * console.log('Filter panel closed');\n * },\n * });\n * ```\n *\n * @see {@link ShellConfig} for shell configuration\n */\nexport interface ToolPanelDefinition {\n /** Unique panel ID */\n id: string;\n /** Panel title shown in accordion header */\n title: string;\n /** Icon for accordion section header (optional, emoji or SVG) */\n icon?: string;\n /** Tooltip for accordion section header */\n tooltip?: string;\n /** Panel content factory - called when panel section opens */\n render: (container: HTMLElement) => void | (() => void);\n /** Called when panel closes (for cleanup) */\n onClose?: () => void;\n /** Panel order priority (lower = first, default: 100) */\n order?: number;\n}\n\n/**\n * Header content definition for plugins contributing to shell header center section.\n *\n * Register via `grid.registerHeaderContent()` to add content between\n * the title and toolbar buttons.\n *\n * @example\n * ```typescript\n * grid.registerHeaderContent({\n * id: 'row-count',\n * order: 10,\n * render: (container) => {\n * const span = document.createElement('span');\n * span.className = 'row-count';\n * span.textContent = `${grid.rows.length} rows`;\n * container.appendChild(span);\n *\n * // Update on data changes\n * const update = () => span.textContent = `${grid.rows.length} rows`;\n * grid.addEventListener('data-change', update);\n *\n * return () => {\n * grid.removeEventListener('data-change', update);\n * };\n * },\n * });\n * ```\n *\n * @see {@link ShellConfig} for shell configuration\n */\nexport interface HeaderContentDefinition {\n /** Unique content ID */\n id: string;\n /** Content factory - called once when shell header renders */\n render: (container: HTMLElement) => void | (() => void);\n /** Called when content is removed (for cleanup) */\n onDestroy?: () => void;\n /** Order priority (lower = first, default: 100) */\n order?: number;\n}\n// #endregion\n\n// #region Column State (Persistence)\n\n/**\n * State for a single column. Captures user-driven changes at runtime.\n * Plugins can extend this interface via module augmentation to add their own state.\n *\n * Used with `grid.getColumnState()` and `grid.columnState` for persisting\n * user customizations (column widths, order, visibility, sort).\n *\n * @example\n * ```typescript\n * // Save column state to localStorage\n * const state = grid.getColumnState();\n * localStorage.setItem('gridState', JSON.stringify(state));\n *\n * // Restore on page load\n * const saved = localStorage.getItem('gridState');\n * if (saved) {\n * grid.columnState = JSON.parse(saved);\n * }\n *\n * // Example column state structure\n * const state: GridColumnState = {\n * columns: [\n * { field: 'name', order: 0, width: 200, hidden: false },\n * { field: 'email', order: 1, width: 300, hidden: false },\n * { field: 'phone', order: 2, hidden: true }, // Hidden column\n * ],\n * sort: { field: 'name', direction: 1 },\n * };\n * ```\n *\n * @example\n * ```typescript\n * // Plugin augmentation example (in filtering plugin)\n * declare module '@toolbox-web/grid' {\n * interface ColumnState {\n * filter?: FilterValue;\n * }\n * }\n * ```\n *\n * @see {@link GridColumnState} for the full state object\n */\nexport interface ColumnState {\n /** Column field identifier */\n field: string;\n /** Position index after reordering (0-based) */\n order: number;\n /** Width in pixels (undefined = use default) */\n width?: number;\n /** Visibility state */\n visible: boolean;\n /** Sort state (undefined = not sorted). */\n sort?: ColumnSortState;\n}\n\n/**\n * Sort state for a column.\n * Used within {@link ColumnState} to track sort direction and priority.\n *\n * @see {@link ColumnState} for column state persistence\n * @see {@link SortChangeDetail} for sort change events\n */\nexport interface ColumnSortState {\n /** Sort direction */\n direction: 'asc' | 'desc';\n /** Priority for multi-sort (0 = primary, 1 = secondary, etc.) */\n priority: number;\n}\n\n/**\n * Complete grid column state for persistence.\n * Contains state for all columns, including plugin-contributed properties.\n *\n * @example\n * ```typescript\n * // Save state\n * const state = grid.getColumnState();\n * localStorage.setItem('grid-state', JSON.stringify(state));\n *\n * // Restore state\n * grid.columnState = JSON.parse(localStorage.getItem('grid-state'));\n * ```\n *\n * @see {@link ColumnState} for individual column state\n * @see {@link PublicGrid.getColumnState} for retrieving state\n */\nexport interface GridColumnState {\n /** Array of column states. */\n columns: ColumnState[];\n}\n// #endregion\n\n// #region Public Event Detail Interfaces\n/**\n * Detail for a cell click event.\n * Provides full context about the clicked cell including row data.\n *\n * @example\n * ```typescript\n * grid.addEventListener('cell-click', (e: CustomEvent<CellClickDetail>) => {\n * const { row, field, value, rowIndex, colIndex } = e.detail;\n * console.log(`Clicked ${field} = ${value} in row ${rowIndex}`);\\n *\n * // Access the full row data\n * if (row.status === 'pending') {\n * showApprovalDialog(row);\n * }\n * });\n * ```\n *\n * @category Events\n */\nexport interface CellClickDetail<TRow = unknown> {\n /** Zero-based row index of the clicked cell. */\n rowIndex: number;\n /** Zero-based column index of the clicked cell. */\n colIndex: number;\n /** Column configuration object for the clicked cell. */\n column: ColumnConfig<TRow>;\n /** Field name of the clicked column. */\n field: string;\n /** Cell value at the clicked position. */\n value: unknown;\n /** Full row data object. */\n row: TRow;\n /** The clicked cell element. */\n cellEl: HTMLElement;\n /** The original mouse event. */\n originalEvent: MouseEvent;\n}\n\n/**\n * Detail for a row click event.\n * Provides context about the clicked row.\n *\n * @example\n * ```typescript\n * grid.addEventListener('row-click', (e: CustomEvent<RowClickDetail>) => {\n * const { row, rowIndex, rowEl } = e.detail;\n * console.log(`Clicked row ${rowIndex}: ${row.name}`);\n *\n * // Highlight the row\n * rowEl.classList.add('selected');\n *\n * // Open detail panel\n * showDetailPanel(row);\n * });\n * ```\n *\n * @category Events\n */\nexport interface RowClickDetail<TRow = unknown> {\n /** Zero-based row index of the clicked row. */\n rowIndex: number;\n /** Full row data object. */\n row: TRow;\n /** The clicked row element. */\n rowEl: HTMLElement;\n /** The original mouse event. */\n originalEvent: MouseEvent;\n}\n\n/**\n * Detail for a sort change (direction 0 indicates cleared sort).\n *\n * @example\n * ```typescript\n * grid.addEventListener('sort-change', (e: CustomEvent<SortChangeDetail>) => {\n * const { field, direction } = e.detail;\n *\n * if (direction === 0) {\n * console.log(`Sort cleared on ${field}`);\n * } else {\n * const dir = direction === 1 ? 'ascending' : 'descending';\n * console.log(`Sorted by ${field} ${dir}`);\n * }\n *\n * // Fetch sorted data from server\n * fetchData({ sortBy: field, sortDir: direction });\n * });\n * ```\n *\n * @see {@link SortState} for the sort state object\n * @see {@link SortHandler} for custom sort handlers\n * @category Events\n */\nexport interface SortChangeDetail {\n /** Sorted field key. */\n field: string;\n /** Direction: 1 ascending, -1 descending, 0 cleared. */\n direction: 1 | -1 | 0;\n}\n\n/**\n * Column resize event detail containing final pixel width.\n *\n * @example\n * ```typescript\n * grid.addEventListener('column-resize', (e: CustomEvent<ColumnResizeDetail>) => {\n * const { field, width } = e.detail;\n * console.log(`Column ${field} resized to ${width}px`);\n *\n * // Persist to user preferences\n * saveColumnWidth(field, width);\n * });\n * ```\n *\n * @see {@link ColumnState} for persisting column state\n * @see {@link ResizeController} for resize implementation\n * @category Events\n */\nexport interface ColumnResizeDetail {\n /** Resized column field key. */\n field: string;\n /** New width in pixels. */\n width: number;\n}\n\n/**\n * Trigger type for cell activation.\n * - `'keyboard'`: Enter key pressed on focused cell\n * - `'pointer'`: Mouse/touch/pen click on cell\n *\n * @see {@link CellActivateDetail} for the activation event detail\n * @category Events\n */\nexport type CellActivateTrigger = 'keyboard' | 'pointer';\n\n/**\n * Fired when a cell is activated by user interaction (Enter key or click).\n * Unified event for both keyboard and pointer activation.\n *\n * @example\n * ```typescript\n * grid.addEventListener('cell-activate', (e: CustomEvent<CellActivateDetail>) => {\n * const { row, field, value, trigger, cellEl } = e.detail;\n *\n * if (trigger === 'keyboard') {\n * console.log('Activated via Enter key');\n * } else {\n * console.log('Activated via click/tap');\n * }\n *\n * // Start custom editing for specific columns\n * if (field === 'notes') {\n * openNotesEditor(row, cellEl);\n * e.preventDefault(); // Prevent default editing\n * }\n * });\n * ```\n *\n * @see {@link CellClickDetail} for click-only events\n * @see {@link CellActivateTrigger} for trigger types\n * @category Events\n */\nexport interface CellActivateDetail<TRow = unknown> {\n /** Zero-based row index of the activated cell. */\n rowIndex: number;\n /** Zero-based column index of the activated cell. */\n colIndex: number;\n /** Field name of the activated column. */\n field: string;\n /** Cell value at the activated position. */\n value: unknown;\n /** Full row data object. */\n row: TRow;\n /** The activated cell element. */\n cellEl: HTMLElement;\n /** What triggered the activation. */\n trigger: CellActivateTrigger;\n /** The original event (KeyboardEvent for keyboard, MouseEvent/PointerEvent for pointer). */\n originalEvent: KeyboardEvent | MouseEvent | PointerEvent;\n}\n\n/**\n * @deprecated Use `CellActivateDetail` instead. Will be removed in next major version.\n * Kept for backwards compatibility.\n *\n * @category Events\n */\nexport interface ActivateCellDetail {\n /** Zero-based row index now focused. */\n row: number;\n /** Zero-based column index now focused. */\n col: number;\n}\n\n/**\n * Event detail for mounting external view renderers.\n *\n * Emitted when a cell uses an external component spec (React, Angular, Vue)\n * and needs the framework adapter to mount the component.\n *\n * @example\n * ```typescript\n * // Framework adapter listens for this event\n * grid.addEventListener('mount-external-view', (e: CustomEvent<ExternalMountViewDetail>) => {\n * const { placeholder, spec, context } = e.detail;\n * // Mount framework component into placeholder\n * mountComponent(spec.component, placeholder, context);\n * });\n * ```\n *\n * @see {@link ColumnConfig.externalView} for external view spec\n * @see {@link FrameworkAdapter} for adapter interface\n * @category Framework Adapters\n */\nexport interface ExternalMountViewDetail<TRow = unknown> {\n placeholder: HTMLElement;\n spec: unknown;\n context: { row: TRow; value: unknown; field: string; column: unknown };\n}\n\n/**\n * Event detail for mounting external editor renderers.\n *\n * Emitted when a cell uses an external editor component spec and needs\n * the framework adapter to mount the editor with commit/cancel bindings.\n *\n * @example\n * ```typescript\n * // Framework adapter listens for this event\n * grid.addEventListener('mount-external-editor', (e: CustomEvent<ExternalMountEditorDetail>) => {\n * const { placeholder, spec, context } = e.detail;\n * // Mount framework editor with commit/cancel wired\n * mountEditor(spec.component, placeholder, {\n * value: context.value,\n * onCommit: context.commit,\n * onCancel: context.cancel,\n * });\n * });\n * ```\n *\n * @see {@link ColumnEditorSpec} for external editor spec\n * @see {@link FrameworkAdapter} for adapter interface\n * @category Framework Adapters\n */\nexport interface ExternalMountEditorDetail<TRow = unknown> {\n placeholder: HTMLElement;\n spec: unknown;\n context: {\n row: TRow;\n value: unknown;\n field: string;\n column: unknown;\n commit: (v: unknown) => void;\n cancel: () => void;\n };\n}\n\n/**\n * Maps event names to their detail payload types.\n *\n * Use this interface for strongly typed event handling.\n *\n * @example\n * ```typescript\n * // Type-safe event listener\n * function handleEvent<K extends keyof DataGridEventMap>(\n * grid: DataGridElement,\n * event: K,\n * handler: (detail: DataGridEventMap[K]) => void,\n * ): void {\n * grid.addEventListener(event, (e: CustomEvent) => handler(e.detail));\n * }\n *\n * handleEvent(grid, 'cell-click', (detail) => {\n * console.log(detail.field); // Type-safe access\n * });\n * ```\n *\n * @see {@link DataGridCustomEvent} for typed CustomEvent wrapper\n * @see {@link DGEvents} for event name constants\n * @category Events\n */\nexport interface DataGridEventMap<TRow = unknown> {\n 'cell-click': CellClickDetail<TRow>;\n 'row-click': RowClickDetail<TRow>;\n 'cell-activate': CellActivateDetail<TRow>;\n 'cell-change': CellChangeDetail<TRow>;\n 'mount-external-view': ExternalMountViewDetail<TRow>;\n 'mount-external-editor': ExternalMountEditorDetail<TRow>;\n 'sort-change': SortChangeDetail;\n 'column-resize': ColumnResizeDetail;\n /** @deprecated Use 'cell-activate' instead */\n 'activate-cell': ActivateCellDetail;\n 'column-state-change': GridColumnState;\n // Note: 'cell-commit', 'row-commit', 'changed-rows-reset' are added via\n // module augmentation by EditingPlugin when imported\n}\n\n/**\n * Extracts the event detail type for a given event name.\n *\n * Utility type for getting the detail payload type of a specific event.\n *\n * @example\n * ```typescript\n * // Extract detail type for specific event\n * type ClickDetail = DataGridEventDetail<'cell-click', Employee>;\n * // Equivalent to: CellClickDetail<Employee>\n *\n * // Use in generic handler\n * function logDetail<K extends keyof DataGridEventMap>(\n * eventName: K,\n * detail: DataGridEventDetail<K>,\n * ): void {\n * console.log(`${eventName}:`, detail);\n * }\n * ```\n *\n * @see {@link DataGridEventMap} for all event types\n * @category Events\n */\nexport type DataGridEventDetail<K extends keyof DataGridEventMap<unknown>, TRow = unknown> = DataGridEventMap<TRow>[K];\n\n/**\n * Custom event type for DataGrid events with typed detail payload.\n *\n * Use this type when you need to cast or declare event handler parameters.\n *\n * @example\n * ```typescript\n * // Strongly typed event handler\n * function onCellClick(e: DataGridCustomEvent<'cell-click', Employee>): void {\n * const { row, field, value } = e.detail;\n * console.log(`Clicked ${field} = ${value} on ${row.name}`);\n * }\n *\n * grid.addEventListener('cell-click', onCellClick);\n * ```\n *\n * @see {@link DataGridEventMap} for all event types\n * @see {@link DataGridEventDetail} for extracting detail type only\n * @category Events\n */\nexport type DataGridCustomEvent<K extends keyof DataGridEventMap<unknown>, TRow = unknown> = CustomEvent<\n DataGridEventMap<TRow>[K]\n>;\n\n/**\n * Template evaluation context for dynamic templates.\n *\n * @category Plugin Development\n */\nexport interface EvalContext {\n value: unknown;\n row: Record<string, unknown> | null;\n}\n// #endregion\n","/**\n * Base Grid Plugin Class\n *\n * All plugins extend this abstract class.\n * Plugins are instantiated per-grid and manage their own state.\n */\n\n// Injected by Vite at build time from package.json (same as grid.ts)\ndeclare const __GRID_VERSION__: string;\n\nimport type {\n ColumnConfig,\n ColumnState,\n GridPlugin,\n HeaderContentDefinition,\n IconValue,\n ToolPanelDefinition,\n} from '../types';\nimport { DEFAULT_GRID_ICONS } from '../types';\n\n// Re-export shared plugin types for convenience\nexport { PLUGIN_QUERIES } from './types';\nexport type {\n AfterCellRenderContext,\n AfterRowRenderContext,\n CellClickEvent,\n CellCoords,\n CellEditor,\n CellMouseEvent,\n CellRenderer,\n ContextMenuItem,\n ContextMenuParams,\n GridElementRef,\n HeaderClickEvent,\n HeaderRenderer,\n KeyboardModifiers,\n PluginCellRenderContext,\n PluginQuery,\n RowClickEvent,\n ScrollEvent,\n} from './types';\n\nimport type {\n AfterCellRenderContext,\n AfterRowRenderContext,\n CellClickEvent,\n CellEditor,\n CellMouseEvent,\n CellRenderer,\n GridElementRef,\n HeaderClickEvent,\n HeaderRenderer,\n PluginQuery,\n RowClickEvent,\n ScrollEvent,\n} from './types';\n\n/**\n * Grid element interface for plugins.\n * Extends GridElementRef with plugin-specific methods.\n * Note: effectiveConfig is already available from GridElementRef.\n */\nexport interface GridElement extends GridElementRef {\n getPlugin<T extends BaseGridPlugin>(PluginClass: new (...args: any[]) => T): T | undefined;\n getPluginByName(name: string): BaseGridPlugin | undefined;\n /**\n * Get a plugin's state by plugin name.\n * This is a loose-coupling method for plugins to access other plugins' state\n * without importing the plugin class directly.\n * @internal Plugin API\n */\n getPluginState?(name: string): unknown;\n}\n\n/**\n * Header render context for plugin header renderers.\n */\nexport interface PluginHeaderRenderContext {\n /** Column configuration */\n column: ColumnConfig;\n /** Column index */\n colIndex: number;\n}\n\n// ============================================================================\n// Plugin Dependency Types\n// ============================================================================\n\n/**\n * Declares a dependency on another plugin.\n *\n * @category Plugin Development\n * @example\n * ```typescript\n * export class UndoRedoPlugin extends BaseGridPlugin {\n * static readonly dependencies: PluginDependency[] = [\n * { name: 'editing', required: true },\n * ];\n * }\n * ```\n */\nexport interface PluginDependency {\n /**\n * The name of the required plugin (matches the plugin's `name` property).\n * Use string names for loose coupling - avoids static imports.\n */\n name: string;\n\n /**\n * Whether this dependency is required (hard) or optional (soft).\n * - `true`: Plugin cannot function without it. Throws error if missing.\n * - `false`: Plugin works with reduced functionality. Logs info if missing.\n * @default true\n */\n required?: boolean;\n\n /**\n * Human-readable reason for this dependency.\n * Shown in error/info messages to help users understand why.\n * @example \"UndoRedoPlugin needs EditingPlugin to track cell edits\"\n */\n reason?: string;\n}\n\n/**\n * Declares an incompatibility between plugins.\n * When both plugins are loaded, a warning is logged to help users understand the conflict.\n *\n * @category Plugin Development\n */\nexport interface PluginIncompatibility {\n /**\n * The name of the incompatible plugin (matches the plugin's `name` property).\n */\n name: string;\n\n /**\n * Human-readable reason for the incompatibility.\n * Should explain why the plugins conflict and any workarounds.\n * @example \"Responsive card layout does not support row grouping yet\"\n */\n reason: string;\n}\n\n// ============================================================================\n// Plugin Query & Event Definitions\n// ============================================================================\n\n/**\n * Defines a query that a plugin can handle.\n * Other plugins or the grid can send this query type to retrieve data.\n *\n * @category Plugin Development\n *\n * @example\n * ```typescript\n * // In manifest\n * queries: [\n * {\n * type: 'canMoveColumn',\n * description: 'Check if a column can be moved/reordered',\n * },\n * ]\n *\n * // In plugin class\n * handleQuery(query: PluginQuery): unknown {\n * if (query.type === 'canMoveColumn') {\n * return this.canMoveColumn(query.context);\n * }\n * }\n * ```\n */\nexport interface QueryDefinition {\n /**\n * The query type identifier (e.g., 'canMoveColumn', 'getContextMenuItems').\n * Should be unique across all plugins.\n */\n type: string;\n\n /**\n * Human-readable description of what the query does.\n */\n description?: string;\n}\n\n/**\n * Defines an event that a plugin can emit.\n * Other plugins can subscribe to these events via the Event Bus.\n *\n * @category Plugin Development\n *\n * @example\n * ```typescript\n * // In manifest\n * events: [\n * {\n * type: 'filter-change',\n * description: 'Emitted when filter criteria change',\n * },\n * ]\n *\n * // In plugin class - emit\n * this.emitPluginEvent('filter-change', { field: 'name', value: 'Alice' });\n *\n * // In another plugin - subscribe\n * this.on('filter-change', (detail) => console.log('Filter changed:', detail));\n * ```\n */\nexport interface EventDefinition {\n /**\n * The event type identifier (e.g., 'filter-change', 'selection-change').\n * Used when emitting and subscribing to events.\n */\n type: string;\n\n /**\n * Human-readable description of what the event represents.\n */\n description?: string;\n\n /**\n * Whether this event is cancelable via `event.preventDefault()`.\n * @default false\n */\n cancelable?: boolean;\n}\n\n// ============================================================================\n// Plugin Manifest Types\n// ============================================================================\n\n/**\n * Defines a property that a plugin \"owns\" - used for configuration validation.\n * When this property is used without the owning plugin loaded, an error is thrown.\n *\n * @category Plugin Development\n */\nexport interface PluginPropertyDefinition {\n /** The property name on column or grid config */\n property: string;\n /** Whether this is a column-level or config-level property */\n level: 'column' | 'config';\n /** Human-readable description for error messages (e.g., 'the \"editable\" column property') */\n description: string;\n /** Import path hint for error messages (e.g., \"import { EditingPlugin } from '@toolbox-web/grid/plugins/editing';\") */\n importHint?: string;\n /** Custom check for whether property is considered \"used\" (default: truthy value check) */\n isUsed?: (value: unknown) => boolean;\n}\n\n/**\n * A configuration validation rule for detecting invalid/conflicting settings.\n * Plugins declare rules in their manifest; the validator executes them during grid initialization.\n *\n * @category Plugin Development\n * @template TConfig - The plugin's configuration type\n */\nexport interface PluginConfigRule<TConfig = unknown> {\n /** Rule identifier for debugging (e.g., 'selection/range-dblclick') */\n id: string;\n /** Severity: 'error' throws, 'warn' logs warning */\n severity: 'error' | 'warn';\n /** Human-readable message shown when rule is violated */\n message: string;\n /** Predicate returning true if the rule is VIOLATED (i.e., config is invalid) */\n check: (pluginConfig: TConfig) => boolean;\n}\n\n/**\n * Hook names that can have execution priority configured.\n *\n * @category Plugin Development\n */\nexport type HookName =\n | 'processColumns'\n | 'processRows'\n | 'afterRender'\n | 'afterCellRender'\n | 'afterRowRender'\n | 'onCellClick'\n | 'onCellMouseDown'\n | 'onCellMouseMove'\n | 'onCellMouseUp'\n | 'onKeyDown'\n | 'onScroll'\n | 'onScrollRender';\n\n/**\n * Static metadata about a plugin's capabilities and requirements.\n * Declared as a static property on plugin classes.\n *\n * @category Plugin Development\n * @template TConfig - The plugin's configuration type\n *\n * @example\n * ```typescript\n * export class MyPlugin extends BaseGridPlugin<MyConfig> {\n * static override readonly manifest: PluginManifest<MyConfig> = {\n * ownedProperties: [\n * { property: 'myProp', level: 'column', description: 'the \"myProp\" column property' },\n * ],\n * configRules: [\n * { id: 'my-plugin/invalid-combo', severity: 'warn', message: '...', check: (c) => c.a && c.b },\n * ],\n * };\n * readonly name = 'myPlugin';\n * }\n * ```\n */\nexport interface PluginManifest<TConfig = unknown> {\n /**\n * Properties this plugin owns - validated by validate-config.ts.\n * If a user uses one of these properties without loading the plugin, an error is thrown.\n */\n ownedProperties?: PluginPropertyDefinition[];\n\n /**\n * Hook execution priority (higher = later, default 0).\n * Use negative values to run earlier, positive to run later.\n */\n hookPriority?: Partial<Record<HookName, number>>;\n\n /**\n * Configuration validation rules - checked during grid initialization.\n * Rules with severity 'error' throw, 'warn' logs to console.\n */\n configRules?: PluginConfigRule<TConfig>[];\n\n /**\n * Plugins that are incompatible with this plugin.\n * When both plugins are loaded together, a warning is shown.\n *\n * @example\n * ```typescript\n * incompatibleWith: [\n * { name: 'groupingRows', reason: 'Responsive card layout does not support row grouping yet' },\n * ],\n * ```\n */\n incompatibleWith?: PluginIncompatibility[];\n\n /**\n * Queries this plugin can handle.\n * Declares what query types this plugin responds to via `handleQuery()`.\n * This replaces the centralized PLUGIN_QUERIES approach with manifest-declared queries.\n *\n * @example\n * ```typescript\n * queries: [\n * { type: 'canMoveColumn', description: 'Check if a column can be moved' },\n * ],\n * ```\n */\n queries?: QueryDefinition[];\n\n /**\n * Events this plugin can emit.\n * Declares what event types other plugins can subscribe to via `on()`.\n *\n * @example\n * ```typescript\n * events: [\n * { type: 'filter-change', description: 'Emitted when filter criteria change' },\n * ],\n * ```\n */\n events?: EventDefinition[];\n}\n\n/**\n * Abstract base class for all grid plugins.\n *\n * @category Plugin Development\n * @template TConfig - Configuration type for the plugin\n */\nexport abstract class BaseGridPlugin<TConfig = unknown> implements GridPlugin {\n /**\n * Plugin dependencies - declare other plugins this one requires.\n *\n * Dependencies are validated when the plugin is attached.\n * Required dependencies throw an error if missing.\n * Optional dependencies log an info message if missing.\n *\n * @example\n * ```typescript\n * static readonly dependencies: PluginDependency[] = [\n * { name: 'editing', required: true, reason: 'Tracks cell edits for undo/redo' },\n * { name: 'selection', required: false, reason: 'Enables selection-based undo' },\n * ];\n * ```\n */\n static readonly dependencies?: PluginDependency[];\n\n /**\n * Plugin manifest - declares owned properties, config rules, and hook priorities.\n *\n * This is read by the configuration validator to:\n * - Validate that required plugins are loaded when their properties are used\n * - Execute configRules to detect invalid/conflicting settings\n * - Order hook execution based on priority\n *\n * @example\n * ```typescript\n * static override readonly manifest: PluginManifest<MyConfig> = {\n * ownedProperties: [\n * { property: 'myProp', level: 'column', description: 'the \"myProp\" column property' },\n * ],\n * configRules: [\n * { id: 'myPlugin/conflict', severity: 'warn', message: '...', check: (c) => c.a && c.b },\n * ],\n * };\n * ```\n */\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n static readonly manifest?: PluginManifest<any>;\n\n /** Unique plugin identifier (derived from class name by default) */\n abstract readonly name: string;\n\n /**\n * Plugin version - defaults to grid version for built-in plugins.\n * Third-party plugins can override with their own semver.\n */\n readonly version: string = typeof __GRID_VERSION__ !== 'undefined' ? __GRID_VERSION__ : 'dev';\n\n /** CSS styles to inject into the grid's shadow DOM */\n readonly styles?: string;\n\n /** Custom cell renderers keyed by type name */\n readonly cellRenderers?: Record<string, CellRenderer>;\n\n /** Custom header renderers keyed by type name */\n readonly headerRenderers?: Record<string, HeaderRenderer>;\n\n /** Custom cell editors keyed by type name */\n readonly cellEditors?: Record<string, CellEditor>;\n\n /** The grid instance this plugin is attached to */\n protected grid!: GridElement;\n\n /** Plugin configuration - merged with defaults in attach() */\n protected config!: TConfig;\n\n /** User-provided configuration from constructor */\n protected readonly userConfig: Partial<TConfig>;\n\n /**\n * Plugin-level AbortController for event listener cleanup.\n * Created fresh in attach(), aborted in detach().\n * This ensures event listeners are properly cleaned up when plugins are re-attached.\n */\n #abortController?: AbortController;\n\n /**\n * Default configuration - subclasses should override this getter.\n * Note: This must be a getter (not property initializer) for proper inheritance\n * since property initializers run after parent constructor.\n */\n protected get defaultConfig(): Partial<TConfig> {\n return {};\n }\n\n constructor(config: Partial<TConfig> = {}) {\n this.userConfig = config;\n }\n\n /**\n * Called when the plugin is attached to a grid.\n * Override to set up event listeners, initialize state, etc.\n *\n * @example\n * ```ts\n * attach(grid: GridElement): void {\n * super.attach(grid);\n * // Set up document-level listeners with auto-cleanup\n * document.addEventListener('keydown', this.handleEscape, {\n * signal: this.disconnectSignal\n * });\n * }\n * ```\n */\n attach(grid: GridElement): void {\n // Abort any previous abort controller (in case of re-attach without detach)\n this.#abortController?.abort();\n // Create fresh abort controller for this attachment\n this.#abortController = new AbortController();\n\n this.grid = grid;\n // Merge config here (after subclass construction is complete)\n this.config = { ...this.defaultConfig, ...this.userConfig } as TConfig;\n }\n\n /**\n * Called when the plugin is detached from a grid.\n * Override to clean up event listeners, timers, etc.\n *\n * @example\n * ```ts\n * detach(): void {\n * // Clean up any state not handled by disconnectSignal\n * this.selectedRows.clear();\n * this.cache = null;\n * }\n * ```\n */\n detach(): void {\n // Abort the plugin's abort controller to clean up all event listeners\n // registered with { signal: this.disconnectSignal }\n this.#abortController?.abort();\n this.#abortController = undefined;\n // Override in subclass for additional cleanup\n }\n\n /**\n * Called when another plugin is attached to the same grid.\n * Use for inter-plugin coordination, e.g., to integrate with new plugins.\n *\n * @param name - The name of the plugin that was attached\n * @param plugin - The plugin instance (for direct access if needed)\n *\n * @example\n * ```ts\n * onPluginAttached(name: string, plugin: BaseGridPlugin): void {\n * if (name === 'selection') {\n * // Integrate with selection plugin\n * this.selectionPlugin = plugin as SelectionPlugin;\n * }\n * }\n * ```\n */\n onPluginAttached?(name: string, plugin: BaseGridPlugin): void;\n\n /**\n * Called when another plugin is detached from the same grid.\n * Use to clean up inter-plugin references.\n *\n * @param name - The name of the plugin that was detached\n *\n * @example\n * ```ts\n * onPluginDetached(name: string): void {\n * if (name === 'selection') {\n * this.selectionPlugin = undefined;\n * }\n * }\n * ```\n */\n onPluginDetached?(name: string): void;\n\n /**\n * Get another plugin instance from the same grid.\n * Use for inter-plugin communication.\n *\n * @example\n * ```ts\n * const selection = this.getPlugin(SelectionPlugin);\n * if (selection) {\n * const selectedRows = selection.getSelectedRows();\n * }\n * ```\n */\n protected getPlugin<T extends BaseGridPlugin>(PluginClass: new (...args: any[]) => T): T | undefined {\n return this.grid?.getPlugin(PluginClass);\n }\n\n /**\n * Emit a custom event from the grid.\n */\n protected emit<T>(eventName: string, detail: T): void {\n this.grid?.dispatchEvent?.(new CustomEvent(eventName, { detail, bubbles: true }));\n }\n\n /**\n * Emit a cancelable custom event from the grid.\n * @returns `true` if the event was cancelled (preventDefault called), `false` otherwise\n */\n protected emitCancelable<T>(eventName: string, detail: T): boolean {\n const event = new CustomEvent(eventName, { detail, bubbles: true, cancelable: true });\n this.grid?.dispatchEvent?.(event);\n return event.defaultPrevented;\n }\n\n // =========================================================================\n // Event Bus - Plugin-to-Plugin Communication\n // =========================================================================\n\n /**\n * Subscribe to an event from another plugin.\n * The subscription is automatically cleaned up when this plugin is detached.\n *\n * @category Plugin Development\n * @param eventType - The event type to listen for (e.g., 'filter-change')\n * @param callback - The callback to invoke when the event is emitted\n *\n * @example\n * ```typescript\n * // In attach() or other initialization\n * this.on('filter-change', (detail) => {\n * console.log('Filter changed:', detail);\n * });\n * ```\n */\n protected on<T = unknown>(eventType: string, callback: (detail: T) => void): void {\n this.grid?._pluginManager?.subscribe(this, eventType, callback as (detail: unknown) => void);\n }\n\n /**\n * Unsubscribe from a plugin event.\n *\n * @category Plugin Development\n * @param eventType - The event type to stop listening for\n *\n * @example\n * ```typescript\n * this.off('filter-change');\n * ```\n */\n protected off(eventType: string): void {\n this.grid?._pluginManager?.unsubscribe(this, eventType);\n }\n\n /**\n * Emit an event to other plugins via the Event Bus.\n * This is for inter-plugin communication only; it does NOT dispatch DOM events.\n * Use `emit()` to dispatch DOM events that external consumers can listen to.\n *\n * @category Plugin Development\n * @param eventType - The event type to emit (should be declared in manifest.events)\n * @param detail - The event payload\n *\n * @example\n * ```typescript\n * // Emit to other plugins (not DOM)\n * this.emitPluginEvent('filter-change', { field: 'name', value: 'Alice' });\n *\n * // For DOM events that consumers can addEventListener to:\n * this.emit('filter-change', { field: 'name', value: 'Alice' });\n * ```\n */\n protected emitPluginEvent<T>(eventType: string, detail: T): void {\n this.grid?._pluginManager?.emitPluginEvent(eventType, detail);\n }\n\n /**\n * Request a re-render of the grid.\n * Uses ROWS phase - does NOT trigger processColumns hooks.\n */\n protected requestRender(): void {\n this.grid?.requestRender?.();\n }\n\n /**\n * Request a columns re-render of the grid.\n * Uses COLUMNS phase - triggers processColumns hooks.\n * Use this when your plugin needs to reprocess column configuration.\n */\n protected requestColumnsRender(): void {\n (this.grid as { requestColumnsRender?: () => void })?.requestColumnsRender?.();\n }\n\n /**\n * Request a re-render and restore focus styling afterward.\n * Use this when a plugin action (like expand/collapse) triggers a render\n * but needs to maintain keyboard navigation focus.\n */\n protected requestRenderWithFocus(): void {\n this.grid?.requestRenderWithFocus?.();\n }\n\n /**\n * Request a lightweight style update without rebuilding DOM.\n * Use this instead of requestRender() when only CSS classes need updating.\n */\n protected requestAfterRender(): void {\n this.grid?.requestAfterRender?.();\n }\n\n /**\n * Get the current rows from the grid.\n */\n protected get rows(): any[] {\n return this.grid?.rows ?? [];\n }\n\n /**\n * Get the original unfiltered/unprocessed rows from the grid.\n * Use this when you need all source data regardless of active filters.\n */\n protected get sourceRows(): any[] {\n return this.grid?.sourceRows ?? [];\n }\n\n /**\n * Get the current columns from the grid.\n */\n protected get columns(): ColumnConfig[] {\n return this.grid?.columns ?? [];\n }\n\n /**\n * Get only visible columns from the grid (excludes hidden).\n * Use this for rendering that needs to match the grid template.\n */\n protected get visibleColumns(): ColumnConfig[] {\n return this.grid?._visibleColumns ?? [];\n }\n\n /**\n * Get the grid as an HTMLElement for direct DOM operations.\n * Use sparingly - prefer the typed GridElementRef API when possible.\n *\n * @example\n * ```ts\n * const width = this.gridElement.clientWidth;\n * this.gridElement.classList.add('my-plugin-active');\n * ```\n */\n protected get gridElement(): HTMLElement {\n return this.grid as unknown as HTMLElement;\n }\n\n /**\n * Get the disconnect signal for event listener cleanup.\n * This signal is aborted when the grid disconnects from the DOM.\n * Use this when adding event listeners that should be cleaned up automatically.\n *\n * Best for:\n * - Document/window-level listeners added in attach()\n * - Listeners on the grid element itself\n * - Any listener that should persist across renders\n *\n * Not needed for:\n * - Listeners on elements created in afterRender() (removed with element)\n *\n * @example\n * element.addEventListener('click', handler, { signal: this.disconnectSignal });\n * document.addEventListener('keydown', handler, { signal: this.disconnectSignal });\n */\n protected get disconnectSignal(): AbortSignal {\n // Return the plugin's own abort signal for proper cleanup on detach/re-attach\n // Falls back to grid's signal if plugin's controller not yet created\n return this.#abortController?.signal ?? this.grid?.disconnectSignal;\n }\n\n /**\n * Get the grid-level icons configuration.\n * Returns merged icons (user config + defaults).\n */\n protected get gridIcons(): typeof DEFAULT_GRID_ICONS {\n const userIcons = this.grid?.gridConfig?.icons ?? {};\n return { ...DEFAULT_GRID_ICONS, ...userIcons };\n }\n\n // #region Animation Helpers\n\n /**\n * Check if animations are enabled at the grid level.\n * Respects gridConfig.animation.mode and the CSS variable set by the grid.\n *\n * Plugins should use this to skip animations when:\n * - Animation mode is 'off' or `false`\n * - User prefers reduced motion and mode is 'reduced-motion' (default)\n *\n * @example\n * ```ts\n * private get animationStyle(): 'slide' | 'fade' | false {\n * if (!this.isAnimationEnabled) return false;\n * return this.config.animation ?? 'slide';\n * }\n * ```\n */\n protected get isAnimationEnabled(): boolean {\n const mode = this.grid?.effectiveConfig?.animation?.mode ?? 'reduced-motion';\n\n // Explicit off = disabled\n if (mode === false || mode === 'off') return false;\n\n // Explicit on = always enabled\n if (mode === true || mode === 'on') return true;\n\n // reduced-motion: check CSS variable (set by grid based on media query)\n const host = this.gridElement;\n if (host) {\n const enabled = getComputedStyle(host).getPropertyValue('--tbw-animation-enabled').trim();\n return enabled !== '0';\n }\n\n return true; // Default to enabled\n }\n\n /**\n * Get the animation duration in milliseconds from CSS variable.\n * Falls back to 200ms if not set.\n *\n * Plugins can use this for their animation timing to stay consistent\n * with the grid-level animation.duration setting.\n *\n * @example\n * ```ts\n * element.animate(keyframes, { duration: this.animationDuration });\n * ```\n */\n protected get animationDuration(): number {\n const host = this.gridElement;\n if (host) {\n const durationStr = getComputedStyle(host).getPropertyValue('--tbw-animation-duration').trim();\n const parsed = parseInt(durationStr, 10);\n if (!isNaN(parsed)) return parsed;\n }\n return 200; // Default\n }\n\n // #endregion\n\n /**\n * Resolve an icon value to string or HTMLElement.\n * Checks plugin config first, then grid-level icons, then defaults.\n *\n * @param iconKey - The icon key in GridIcons (e.g., 'expand', 'collapse')\n * @param pluginOverride - Optional plugin-level override\n * @returns The resolved icon value\n */\n protected resolveIcon(iconKey: keyof typeof DEFAULT_GRID_ICONS, pluginOverride?: IconValue): IconValue {\n // Plugin override takes precedence\n if (pluginOverride !== undefined) {\n return pluginOverride;\n }\n // Then grid-level config\n return this.gridIcons[iconKey];\n }\n\n /**\n * Set an icon value on an element.\n * Handles both string (text/HTML) and HTMLElement values.\n *\n * @param element - The element to set the icon on\n * @param icon - The icon value (string or HTMLElement)\n */\n protected setIcon(element: HTMLElement, icon: IconValue): void {\n if (typeof icon === 'string') {\n element.innerHTML = icon;\n } else if (icon instanceof HTMLElement) {\n element.innerHTML = '';\n element.appendChild(icon.cloneNode(true));\n }\n }\n\n /**\n * Log a warning message.\n */\n protected warn(message: string): void {\n console.warn(`[tbw-grid:${this.name}] ${message}`);\n }\n\n // #region Lifecycle Hooks\n\n /**\n * Transform rows before rendering.\n * Called during each render cycle before rows are rendered to the DOM.\n * Use this to filter, sort, or add computed properties to rows.\n *\n * @param rows - The current rows array (readonly to encourage returning a new array)\n * @returns The modified rows array to render\n *\n * @example\n * ```ts\n * processRows(rows: readonly any[]): any[] {\n * // Filter out hidden rows\n * return rows.filter(row => !row._hidden);\n * }\n * ```\n *\n * @example\n * ```ts\n * processRows(rows: readonly any[]): any[] {\n * // Add computed properties\n * return rows.map(row => ({\n * ...row,\n * _fullName: `${row.firstName} ${row.lastName}`\n * }));\n * }\n * ```\n */\n processRows?(rows: readonly any[]): any[];\n\n /**\n * Transform columns before rendering.\n * Called during each render cycle before column headers and cells are rendered.\n * Use this to add, remove, or modify column definitions.\n *\n * @param columns - The current columns array (readonly to encourage returning a new array)\n * @returns The modified columns array to render\n *\n * @example\n * ```ts\n * processColumns(columns: readonly ColumnConfig[]): ColumnConfig[] {\n * // Add a selection checkbox column\n * return [\n * { field: '_select', header: '', width: 40 },\n * ...columns\n * ];\n * }\n * ```\n */\n processColumns?(columns: readonly ColumnConfig[]): ColumnConfig[];\n\n /**\n * Called before each render cycle begins.\n * Use this to prepare state or cache values needed during rendering.\n *\n * **Note:** This hook is currently a placeholder for future implementation.\n * It is defined in the interface but not yet invoked by the grid's render pipeline.\n * If you need this functionality, please open an issue or contribute an implementation.\n *\n * @example\n * ```ts\n * beforeRender(): void {\n * this.visibleRowCount = this.calculateVisibleRows();\n * }\n * ```\n */\n beforeRender?(): void;\n\n /**\n * Called after each render cycle completes.\n * Use this for DOM manipulation, adding event listeners to rendered elements,\n * or applying visual effects like selection highlights.\n *\n * @example\n * ```ts\n * afterRender(): void {\n * // Apply selection styling to rendered rows\n * const rows = this.gridElement?.querySelectorAll('.data-row');\n * rows?.forEach((row, i) => {\n * row.classList.toggle('selected', this.selectedRows.has(i));\n * });\n * }\n * ```\n */\n afterRender?(): void;\n\n /**\n * Called after each cell is rendered.\n * This hook is more efficient than `afterRender()` for cell-level modifications\n * because you receive the cell context directly - no DOM queries needed.\n *\n * Use cases:\n * - Adding selection/highlight classes to specific cells\n * - Injecting badges or decorations\n * - Applying conditional styling based on cell value\n *\n * Performance note: Called for every visible cell during render. Keep implementation fast.\n * This hook is also called during scroll when cells are recycled.\n *\n * @param context - The cell render context with row, column, value, and elements\n *\n * @example\n * ```ts\n * afterCellRender(context: AfterCellRenderContext): void {\n * // Add selection class without DOM queries\n * if (this.isSelected(context.rowIndex, context.colIndex)) {\n * context.cellElement.classList.add('selected');\n * }\n *\n * // Add validation error styling\n * if (this.hasError(context.row, context.column.field)) {\n * context.cellElement.classList.add('has-error');\n * }\n * }\n * ```\n */\n afterCellRender?(context: AfterCellRenderContext): void;\n\n /**\n * Called after a row is fully rendered (all cells complete).\n * Use this for row-level decorations, styling, or ARIA attributes.\n *\n * Common use cases:\n * - Adding selection classes to entire rows (row-focus, selected)\n * - Setting row-level ARIA attributes\n * - Applying row validation highlighting\n * - Tree indentation styling\n *\n * Performance note: Called for every visible row during render. Keep implementation fast.\n * This hook is also called during scroll when rows are recycled.\n *\n * @param context - The row render context with row data and element\n *\n * @example\n * ```ts\n * afterRowRender(context: AfterRowRenderContext): void {\n * // Add row selection class without DOM queries\n * if (this.isRowSelected(context.rowIndex)) {\n * context.rowElement.classList.add('selected', 'row-focus');\n * }\n *\n * // Add validation error styling\n * if (this.rowHasErrors(context.row)) {\n * context.rowElement.classList.add('has-errors');\n * }\n * }\n * ```\n */\n afterRowRender?(context: AfterRowRenderContext): void;\n\n /**\n * Called after scroll-triggered row rendering completes.\n * This is a lightweight hook for applying visual state to recycled DOM elements.\n * Use this instead of afterRender when you need to reapply styling during scroll.\n *\n * Performance note: This is called frequently during scroll. Keep implementation fast.\n *\n * @example\n * ```ts\n * onScrollRender(): void {\n * // Reapply selection state to visible cells\n * this.applySelectionToVisibleCells();\n * }\n * ```\n */\n onScrollRender?(): void;\n\n /**\n * Return extra height contributed by this plugin (e.g., expanded detail rows).\n * Used to adjust scrollbar height calculations for virtualization.\n *\n * @returns Total extra height in pixels\n *\n * @deprecated Use {@link getRowHeight} instead. This hook will be removed in v3.0.\n * The new `getRowHeight(row, index)` hook provides per-row height information which\n * enables better position caching and variable row height support.\n *\n * @example\n * ```ts\n * // OLD (deprecated):\n * getExtraHeight(): number {\n * return this.expandedRows.size * this.detailHeight;\n * }\n *\n * // NEW (preferred):\n * getRowHeight(row: unknown, index: number): number | undefined {\n * if (this.isExpanded(row)) {\n * return this.baseRowHeight + this.getDetailHeight(row);\n * }\n * return undefined;\n * }\n * ```\n */\n getExtraHeight?(): number;\n\n /**\n * Return extra height that appears before a given row index.\n * Used by virtualization to correctly calculate scroll positions when\n * there's variable height content (like expanded detail rows) above the viewport.\n *\n * @param beforeRowIndex - The row index to calculate extra height before\n * @returns Extra height in pixels that appears before this row\n *\n * @deprecated Use {@link getRowHeight} instead. This hook will be removed in v3.0.\n * The new `getRowHeight(row, index)` hook provides per-row height information which\n * enables better position caching and variable row height support.\n *\n * @example\n * ```ts\n * // OLD (deprecated):\n * getExtraHeightBefore(beforeRowIndex: number): number {\n * let height = 0;\n * for (const expandedRowIndex of this.expandedRowIndices) {\n * if (expandedRowIndex < beforeRowIndex) {\n * height += this.getDetailHeight(expandedRowIndex);\n * }\n * }\n * return height;\n * }\n *\n * // NEW (preferred):\n * getRowHeight(row: unknown, index: number): number | undefined {\n * if (this.isExpanded(row)) {\n * return this.baseRowHeight + this.getDetailHeight(row);\n * }\n * return undefined;\n * }\n * ```\n */\n getExtraHeightBefore?(beforeRowIndex: number): number;\n\n /**\n * Get the height of a specific row.\n * Used for synthetic rows (group headers, detail panels, etc.) that have fixed heights.\n * Return undefined if this plugin does not manage the height for this row.\n *\n * This hook is called during position cache rebuilds for variable row height virtualization.\n * Plugins that create synthetic rows should implement this to provide accurate heights.\n *\n * @param row - The row data\n * @param index - The row index in the processed rows array\n * @returns The row height in pixels, or undefined if not managed by this plugin\n *\n * @example\n * ```ts\n * getRowHeight(row: unknown, index: number): number | undefined {\n * // Group headers have a fixed height\n * if (this.isGroupHeader(row)) {\n * return 32;\n * }\n * return undefined; // Let grid use default/measured height\n * }\n * ```\n */\n getRowHeight?(row: unknown, index: number): number | undefined;\n\n /**\n * Adjust the virtualization start index to render additional rows before the visible range.\n * Use this when expanded content (like detail rows) needs its parent row to remain rendered\n * even when the parent row itself has scrolled above the viewport.\n *\n * @param start - The calculated start row index\n * @param scrollTop - The current scroll position\n * @param rowHeight - The height of a single row\n * @returns The adjusted start index (lower than or equal to original start)\n *\n * @example\n * ```ts\n * adjustVirtualStart(start: number, scrollTop: number, rowHeight: number): number {\n * // If row 5 is expanded and scrolled partially, keep it rendered\n * for (const expandedRowIndex of this.expandedRowIndices) {\n * const expandedRowTop = expandedRowIndex * rowHeight;\n * const expandedRowBottom = expandedRowTop + rowHeight + this.detailHeight;\n * if (expandedRowBottom > scrollTop && expandedRowIndex < start) {\n * return expandedRowIndex;\n * }\n * }\n * return start;\n * }\n * ```\n */\n adjustVirtualStart?(start: number, scrollTop: number, rowHeight: number): number;\n\n /**\n * Render a custom row, bypassing the default row rendering.\n * Use this for special row types like group headers, detail rows, or footers.\n *\n * @param row - The row data object\n * @param rowEl - The row DOM element to render into\n * @param rowIndex - The index of the row in the data array\n * @returns `true` if the plugin handled rendering (prevents default), `false`/`void` for default rendering\n *\n * @example\n * ```ts\n * renderRow(row: any, rowEl: HTMLElement, rowIndex: number): boolean | void {\n * if (row._isGroupHeader) {\n * rowEl.innerHTML = `<div class=\"group-header\">${row._groupLabel}</div>`;\n * return true; // Handled - skip default rendering\n * }\n * // Return void to let default rendering proceed\n * }\n * ```\n */\n renderRow?(row: any, rowEl: HTMLElement, rowIndex: number): boolean | void;\n\n // #endregion\n\n // #region Inter-Plugin Communication\n\n /**\n * Handle queries from other plugins.\n * This is the generic mechanism for inter-plugin communication.\n * Plugins can respond to well-known query types or define their own.\n *\n * **Prefer `handleQuery` for new plugins** - it has the same signature but\n * a clearer name. `onPluginQuery` is kept for backwards compatibility.\n *\n * @category Plugin Development\n * @param query - The query object with type and context\n * @returns Query-specific response, or undefined if not handling this query\n *\n * @example\n * ```ts\n * onPluginQuery(query: PluginQuery): unknown {\n * switch (query.type) {\n * case PLUGIN_QUERIES.CAN_MOVE_COLUMN:\n * // Prevent moving pinned columns\n * const column = query.context as ColumnConfig;\n * if (column.sticky === 'left' || column.sticky === 'right') {\n * return false;\n * }\n * break;\n * case PLUGIN_QUERIES.GET_CONTEXT_MENU_ITEMS:\n * const params = query.context as ContextMenuParams;\n * return [{ id: 'my-action', label: 'My Action', action: () => {} }];\n * }\n * }\n * ```\n * @deprecated Use `handleQuery` instead for new plugins.\n */\n onPluginQuery?(query: PluginQuery): unknown;\n\n /**\n * Handle queries from other plugins or the grid.\n *\n * Queries are declared in `manifest.queries` and dispatched via `grid.query()`.\n * This enables type-safe, discoverable inter-plugin communication.\n *\n * @category Plugin Development\n * @param query - The query object with type and context\n * @returns Query-specific response, or undefined if not handling this query\n *\n * @example\n * ```ts\n * // In manifest\n * static override readonly manifest: PluginManifest = {\n * queries: [\n * { type: 'canMoveColumn', description: 'Check if a column can be moved' },\n * ],\n * };\n *\n * // In plugin class\n * handleQuery(query: PluginQuery): unknown {\n * if (query.type === 'canMoveColumn') {\n * const column = query.context as ColumnConfig;\n * return !column.sticky; // Can't move sticky columns\n * }\n * }\n * ```\n */\n handleQuery?(query: PluginQuery): unknown;\n\n // #endregion\n\n // #region Interaction Hooks\n\n /**\n * Handle keyboard events on the grid.\n * Called when a key is pressed while the grid or a cell has focus.\n *\n * @param event - The native KeyboardEvent\n * @returns `true` to prevent default behavior and stop propagation, `false`/`void` to allow default\n *\n * @example\n * ```ts\n * onKeyDown(event: KeyboardEvent): boolean | void {\n * // Handle Ctrl+A for select all\n * if (event.ctrlKey && event.key === 'a') {\n * this.selectAllRows();\n * return true; // Prevent default browser select-all\n * }\n * }\n * ```\n */\n onKeyDown?(event: KeyboardEvent): boolean | void;\n\n /**\n * Handle cell click events.\n * Called when a data cell is clicked (not headers).\n *\n * @param event - Cell click event with row/column context\n * @returns `true` to prevent default behavior and stop propagation, `false`/`void` to allow default\n *\n * @example\n * ```ts\n * onCellClick(event: CellClickEvent): boolean | void {\n * if (event.field === '_select') {\n * this.toggleRowSelection(event.rowIndex);\n * return true; // Handled\n * }\n * }\n * ```\n */\n onCellClick?(event: CellClickEvent): boolean | void;\n\n /**\n * Handle row click events.\n * Called when any part of a data row is clicked.\n * Note: This is called in addition to onCellClick, not instead of.\n *\n * @param event - Row click event with row context\n * @returns `true` to prevent default behavior and stop propagation, `false`/`void` to allow default\n *\n * @example\n * ```ts\n * onRowClick(event: RowClickEvent): boolean | void {\n * if (this.config.mode === 'row') {\n * this.selectRow(event.rowIndex, event.originalEvent);\n * return true;\n * }\n * }\n * ```\n */\n onRowClick?(event: RowClickEvent): boolean | void;\n\n /**\n * Handle header click events.\n * Called when a column header is clicked. Commonly used for sorting.\n *\n * @param event - Header click event with column context\n * @returns `true` to prevent default behavior and stop propagation, `false`/`void` to allow default\n *\n * @example\n * ```ts\n * onHeaderClick(event: HeaderClickEvent): boolean | void {\n * if (event.column.sortable !== false) {\n * this.toggleSort(event.field);\n * return true;\n * }\n * }\n * ```\n */\n onHeaderClick?(event: HeaderClickEvent): boolean | void;\n\n /**\n * Handle scroll events on the grid viewport.\n * Called during scrolling. Note: This may be called frequently; debounce if needed.\n *\n * @param event - Scroll event with scroll position and viewport dimensions\n *\n * @example\n * ```ts\n * onScroll(event: ScrollEvent): void {\n * // Update sticky column positions\n * this.updateStickyPositions(event.scrollLeft);\n * }\n * ```\n */\n onScroll?(event: ScrollEvent): void;\n\n /**\n * Handle cell mousedown events.\n * Used for initiating drag operations like range selection or column resize.\n *\n * @param event - Mouse event with cell context\n * @returns `true` to indicate drag started (prevents text selection), `false`/`void` otherwise\n *\n * @example\n * ```ts\n * onCellMouseDown(event: CellMouseEvent): boolean | void {\n * if (event.rowIndex !== undefined && this.config.mode === 'range') {\n * this.startDragSelection(event.rowIndex, event.colIndex);\n * return true; // Prevent text selection\n * }\n * }\n * ```\n */\n onCellMouseDown?(event: CellMouseEvent): boolean | void;\n\n /**\n * Handle cell mousemove events during drag operations.\n * Only called when a drag is in progress (after mousedown returned true).\n *\n * @param event - Mouse event with current cell context\n * @returns `true` to continue handling the drag, `false`/`void` otherwise\n *\n * @example\n * ```ts\n * onCellMouseMove(event: CellMouseEvent): boolean | void {\n * if (this.isDragging && event.rowIndex !== undefined) {\n * this.extendSelection(event.rowIndex, event.colIndex);\n * return true;\n * }\n * }\n * ```\n */\n onCellMouseMove?(event: CellMouseEvent): boolean | void;\n\n /**\n * Handle cell mouseup events to end drag operations.\n *\n * @param event - Mouse event with final cell context\n * @returns `true` if drag was finalized, `false`/`void` otherwise\n *\n * @example\n * ```ts\n * onCellMouseUp(event: CellMouseEvent): boolean | void {\n * if (this.isDragging) {\n * this.finalizeDragSelection();\n * this.isDragging = false;\n * return true;\n * }\n * }\n * ```\n */\n onCellMouseUp?(event: CellMouseEvent): boolean | void;\n\n // Note: Context menu items are provided via handleQuery('getContextMenuItems').\n // This keeps the core decoupled from the context-menu plugin specifics.\n\n // #endregion\n\n // #region Column State Hooks\n\n /**\n * Contribute plugin-specific state for a column.\n * Called by the grid when collecting column state for serialization.\n * Plugins can add their own properties to the column state.\n *\n * @param field - The field name of the column\n * @returns Partial column state with plugin-specific properties, or undefined if no state to contribute\n *\n * @example\n * ```ts\n * getColumnState(field: string): Partial<ColumnState> | undefined {\n * const filterModel = this.filterModels.get(field);\n * if (filterModel) {\n * // Uses module augmentation to add filter property to ColumnState\n * return { filter: filterModel } as Partial<ColumnState>;\n * }\n * return undefined;\n * }\n * ```\n */\n getColumnState?(field: string): Partial<ColumnState> | undefined;\n\n /**\n * Apply plugin-specific state to a column.\n * Called by the grid when restoring column state from serialized data.\n * Plugins should restore their internal state based on the provided state.\n *\n * @param field - The field name of the column\n * @param state - The column state to apply (may contain plugin-specific properties)\n *\n * @example\n * ```ts\n * applyColumnState(field: string, state: ColumnState): void {\n * // Check for filter property added via module augmentation\n * const filter = (state as any).filter;\n * if (filter) {\n * this.filterModels.set(field, filter);\n * this.applyFilter();\n * }\n * }\n * ```\n */\n applyColumnState?(field: string, state: ColumnState): void;\n\n // #endregion\n\n // #region Scroll Boundary Hooks\n\n /**\n * Report horizontal scroll boundary offsets for this plugin.\n * Plugins that obscure part of the scroll area (e.g., pinned/sticky columns)\n * should return how much space they occupy on each side.\n * The keyboard navigation uses this to ensure focused cells are fully visible.\n *\n * @param rowEl - The row element (optional, for calculating widths from rendered cells)\n * @param focusedCell - The currently focused cell element (optional, to determine if scrolling should be skipped)\n * @returns Object with left/right pixel offsets and optional skipScroll flag, or undefined if plugin has no offsets\n *\n * @example\n * ```ts\n * getHorizontalScrollOffsets(rowEl?: HTMLElement, focusedCell?: HTMLElement): { left: number; right: number; skipScroll?: boolean } | undefined {\n * // Calculate total width of left-pinned columns\n * const leftCells = rowEl?.querySelectorAll('.sticky-left') ?? [];\n * let left = 0;\n * leftCells.forEach(el => { left += (el as HTMLElement).offsetWidth; });\n * // Skip scroll if focused cell is pinned (always visible)\n * const skipScroll = focusedCell?.classList.contains('sticky-left');\n * return { left, right: 0, skipScroll };\n * }\n * ```\n */\n getHorizontalScrollOffsets?(\n rowEl?: HTMLElement,\n focusedCell?: HTMLElement,\n ): { left: number; right: number; skipScroll?: boolean } | undefined;\n\n // #endregion\n\n // #region Shell Integration Hooks\n\n /**\n * Register a tool panel for this plugin.\n * Return undefined if plugin has no tool panel.\n * The shell will create a toolbar toggle button and render the panel content\n * when the user opens the panel.\n *\n * @returns Tool panel definition, or undefined if plugin has no panel\n *\n * @example\n * ```ts\n * getToolPanel(): ToolPanelDefinition | undefined {\n * return {\n * id: 'columns',\n * title: 'Columns',\n * icon: '☰',\n * tooltip: 'Show/hide columns',\n * order: 10,\n * render: (container) => {\n * this.renderColumnList(container);\n * return () => this.cleanup();\n * },\n * };\n * }\n * ```\n */\n getToolPanel?(): ToolPanelDefinition | undefined;\n\n /**\n * Register content for the shell header center section.\n * Return undefined if plugin has no header content.\n * Examples: search input, selection summary, status indicators.\n *\n * @returns Header content definition, or undefined if plugin has no header content\n *\n * @example\n * ```ts\n * getHeaderContent(): HeaderContentDefinition | undefined {\n * return {\n * id: 'quick-filter',\n * order: 10,\n * render: (container) => {\n * const input = document.createElement('input');\n * input.type = 'text';\n * input.placeholder = 'Search...';\n * input.addEventListener('input', this.handleInput);\n * container.appendChild(input);\n * return () => input.removeEventListener('input', this.handleInput);\n * },\n * };\n * }\n * ```\n */\n getHeaderContent?(): HeaderContentDefinition | undefined;\n\n // #endregion\n}\n","/**\n * Shared Expander Column Utilities\n *\n * Provides a fixed expander column for plugins that need expand/collapse icons\n * (MasterDetail, Tree, RowGrouping). The column is:\n * - Always first in the grid\n * - Cannot be reordered (lockPosition: true)\n * - Has no header (empty string)\n * - Has no right border (borderless styling)\n * - Narrow width (just fits the icon)\n */\n\nimport type { ColumnConfig } from '../types';\n\n/** Special field name for the expander column */\nexport const EXPANDER_COLUMN_FIELD = '__tbw_expander';\n\n/** Default width for the expander column (pixels) */\nexport const EXPANDER_COLUMN_WIDTH = 32;\n\n/**\n * Marker interface for expander column renderers.\n * Used to detect if expander column is already present.\n */\nexport interface ExpanderColumnRenderer {\n (ctx: any): HTMLElement;\n __expanderColumn?: true;\n /** Plugin name that created this expander */\n __expanderPlugin?: string;\n}\n\n/**\n * Check if a column is an expander column.\n */\nexport function isExpanderColumn(column: ColumnConfig): boolean {\n return column.field === EXPANDER_COLUMN_FIELD;\n}\n\n/**\n * Check if a column is a utility column (excluded from selection, clipboard, etc.).\n * Utility columns are non-data columns like expander columns.\n */\nexport function isUtilityColumn(column: ColumnConfig): boolean {\n return column.meta?.utility === true;\n}\n\n/**\n * Find an existing expander column in the column array.\n */\nexport function findExpanderColumn(columns: readonly ColumnConfig[]): ColumnConfig | undefined {\n return columns.find(isExpanderColumn);\n}\n\n/**\n * Create the base expander column config.\n * Plugins should add their own renderer to customize the expand icon behavior.\n *\n * @param pluginName - Name of the plugin creating the expander (for debugging)\n * @returns Base column config for the expander column\n */\nexport function createExpanderColumnConfig(pluginName: string): ColumnConfig {\n return {\n field: EXPANDER_COLUMN_FIELD as any,\n header: '', // No header text - visually merges with next column\n width: EXPANDER_COLUMN_WIDTH,\n resizable: false,\n sortable: false,\n filterable: false, // No filter button for expander column\n meta: {\n lockPosition: true,\n suppressMovable: true,\n expanderColumn: true,\n expanderPlugin: pluginName,\n utility: true, // Marks this as a utility column (excluded from selection, clipboard, etc.)\n },\n };\n}\n\n/**\n * Create a container element for expand/collapse toggle icons.\n * Used by plugins to wrap their expand icons with consistent styling.\n *\n * @param expanded - Whether the item is currently expanded\n * @param pluginClass - CSS class prefix for the plugin (e.g., 'master-detail', 'tree')\n * @returns Container span element\n */\nexport function createExpanderContainer(expanded: boolean, pluginClass: string): HTMLSpanElement {\n const container = document.createElement('span');\n container.className = `${pluginClass}-expander expander-cell`;\n if (expanded) {\n container.classList.add('expanded');\n }\n return container;\n}\n\n/**\n * CSS styles for the expander column.\n * Plugins should include this in their styles to ensure consistent appearance.\n */\nexport const EXPANDER_COLUMN_STYLES = `\n/* Expander column data cells - always first, borderless right edge */\n.cell[data-field=\"${EXPANDER_COLUMN_FIELD}\"] {\n border-right: none !important;\n padding: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n/* Expander column header - completely hidden, no content, no border, no width contribution */\n.header-row .cell[data-field=\"${EXPANDER_COLUMN_FIELD}\"] {\n visibility: hidden;\n border: none !important;\n padding: 0;\n overflow: hidden;\n}\n\n/* The column after the expander should visually extend into the expander header space */\n.header-row .cell[data-field=\"${EXPANDER_COLUMN_FIELD}\"] + .cell {\n /* Pull left to cover the hidden expander header */\n margin-left: -${EXPANDER_COLUMN_WIDTH}px;\n padding-left: calc(var(--tbw-cell-padding, 8px) + ${EXPANDER_COLUMN_WIDTH}px);\n}\n\n/* Expander cell contents */\n.expander-cell {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 100%;\n height: 100%;\n cursor: pointer;\n}\n`;\n","/**\n * Filtering Plugin (Class-based)\n *\n * Provides comprehensive filtering functionality for tbw-grid.\n * Supports text, number, date, set, and boolean filters with caching.\n * Includes UI with filter buttons in headers and dropdown filter panels.\n */\n\nimport { computeVirtualWindow, shouldBypassVirtualization } from '../../core/internal/virtualization';\nimport { BaseGridPlugin, type GridElement, type PluginManifest, type PluginQuery } from '../../core/plugin/base-plugin';\nimport { isUtilityColumn } from '../../core/plugin/expander-column';\nimport type { ColumnConfig, ColumnState } from '../../core/types';\nimport type { ContextMenuParams, HeaderContextMenuItem } from '../context-menu/types';\nimport { computeFilterCacheKey, filterRows, getUniqueValues, getUniqueValuesBatch } from './filter-model';\nimport styles from './filtering.css?inline';\nimport filterPanelStyles from './FilteringPlugin.css?inline';\nimport type { FilterChangeDetail, FilterConfig, FilterModel, FilterPanelParams } from './types';\n\n/**\n * Filtering Plugin for tbw-grid\n *\n * Adds column header filters with text search, dropdown options, and custom filter panels.\n * Supports both **local filtering** for small datasets and **async handlers** for server-side\n * filtering on large datasets.\n *\n * ## Installation\n *\n * ```ts\n * import { FilteringPlugin } from '@toolbox-web/grid/plugins/filtering';\n * ```\n *\n * ## Configuration Options\n *\n * | Option | Type | Default | Description |\n * |--------|------|---------|-------------|\n * | `debounceMs` | `number` | `300` | Debounce delay for filter input |\n * | `caseSensitive` | `boolean` | `false` | Case-sensitive string matching |\n * | `trimInput` | `boolean` | `true` | Trim whitespace from filter input |\n * | `useWorker` | `boolean` | `true` | Use Web Worker for datasets >1000 rows |\n * | `filterPanelRenderer` | `FilterPanelRenderer` | - | Custom filter panel renderer |\n * | `valuesHandler` | `FilterValuesHandler` | - | Async handler to fetch unique filter values |\n * | `filterHandler` | `FilterHandler<TRow>` | - | Async handler to apply filters remotely |\n *\n * ## Column Configuration\n *\n * | Property | Type | Description |\n * |----------|------|-------------|\n * | `filterable` | `boolean` | Enable filtering for this column |\n * | `filterType` | `'text' \\| 'select' \\| 'number' \\| 'date'` | Filter UI type |\n * | `filterOptions` | `unknown[]` | Predefined options for select filters |\n *\n * ## Programmatic API\n *\n * | Method | Signature | Description |\n * |--------|-----------|-------------|\n * | `setFilter` | `(field, value) => void` | Set filter value for a column |\n * | `getFilters` | `() => FilterModel[]` | Get all current filters |\n * | `clearFilters` | `() => void` | Clear all filters |\n * | `clearFilter` | `(field) => void` | Clear filter for a specific column |\n *\n * ## CSS Custom Properties\n *\n * | Property | Default | Description |\n * |----------|---------|-------------|\n * | `--tbw-filter-panel-bg` | `var(--tbw-color-panel-bg)` | Panel background |\n * | `--tbw-filter-panel-fg` | `var(--tbw-color-fg)` | Panel text color |\n * | `--tbw-filter-panel-border` | `var(--tbw-color-border)` | Panel border |\n * | `--tbw-filter-active-color` | `var(--tbw-color-accent)` | Active filter indicator |\n * | `--tbw-filter-input-bg` | `var(--tbw-color-bg)` | Input background |\n * | `--tbw-filter-input-focus` | `var(--tbw-color-accent)` | Input focus border |\n *\n * @example Basic Usage with Filterable Columns\n * ```ts\n * import '@toolbox-web/grid';\n * import { FilteringPlugin } from '@toolbox-web/grid/plugins/filtering';\n *\n * const grid = document.querySelector('tbw-grid');\n * grid.gridConfig = {\n * columns: [\n * { field: 'name', header: 'Name', filterable: true },\n * { field: 'status', header: 'Status', filterable: true, filterType: 'select' },\n * { field: 'email', header: 'Email', filterable: true },\n * ],\n * plugins: [new FilteringPlugin({ debounceMs: 300 })],\n * };\n * grid.rows = data;\n * ```\n *\n * @example Column Formatters in Filter Panel\n * When a column defines a `format` function, the built-in set filter panel\n * displays formatted labels instead of raw values. Search within the panel\n * also matches against the formatted text.\n * ```ts\n * grid.gridConfig = {\n * columns: [\n * {\n * field: 'price',\n * filterable: true,\n * format: (value) => `$${Number(value).toFixed(2)}`,\n * // Filter checkboxes show \"$9.99\" instead of \"9.99\"\n * },\n * ],\n * plugins: [new FilteringPlugin()],\n * };\n * ```\n *\n * @example Server-Side Filtering with Async Handlers\n * ```ts\n * new FilteringPlugin({\n * // Fetch unique values from server for filter dropdown\n * valuesHandler: async (field, column) => {\n * const response = await fetch(`/api/distinct-values?field=${field}`);\n * return response.json();\n * },\n * // Apply filters on the server\n * filterHandler: async (filters, currentRows) => {\n * const response = await fetch('/api/data', {\n * method: 'POST',\n * body: JSON.stringify({ filters }),\n * });\n * return response.json();\n * },\n * });\n * ```\n *\n * @see {@link FilterConfig} for all configuration options\n * @see {@link FilterModel} for filter data structure\n * @see {@link FilterPanelParams} for custom panel renderer parameters\n *\n * @internal Extends BaseGridPlugin\n */\nexport class FilteringPlugin extends BaseGridPlugin<FilterConfig> {\n /**\n * Plugin manifest - declares events emitted by this plugin.\n * @internal\n */\n static override readonly manifest: PluginManifest = {\n events: [\n {\n type: 'filter-applied',\n description: 'Emitted when filter criteria change. Subscribers can react to row visibility changes.',\n },\n ],\n queries: [\n {\n type: 'getContextMenuItems',\n description: 'Contributes filter-related items to the header context menu',\n },\n ],\n };\n\n /** @internal */\n readonly name = 'filtering';\n /** @internal */\n override readonly styles = styles;\n\n /** @internal */\n protected override get defaultConfig(): Partial<FilterConfig> {\n return {\n debounceMs: 300,\n caseSensitive: false,\n trimInput: true,\n useWorker: true,\n };\n }\n\n // #region Helpers\n\n /**\n * Check if filtering is enabled at the grid level.\n * Grid-wide `filterable: false` disables filtering for all columns.\n */\n private isFilteringEnabled(): boolean {\n return this.grid.effectiveConfig?.filterable !== false;\n }\n\n /**\n * Check if a specific column is filterable, respecting both grid-level and column-level settings.\n */\n private isColumnFilterable(col: { filterable?: boolean; field?: string }): boolean {\n if (!this.isFilteringEnabled()) return false;\n return col.filterable !== false;\n }\n\n /**\n * Build a map of field → filterValue extractor for columns that have one.\n * Used to pass array-aware value extraction to the pure filter functions.\n */\n private getFilterValues():\n | Map<string, (value: unknown, row: Record<string, unknown>) => unknown | unknown[]>\n | undefined {\n const columns = this.grid.effectiveConfig?.columns;\n if (!columns) return undefined;\n\n let map: Map<string, (value: unknown, row: Record<string, unknown>) => unknown | unknown[]> | undefined;\n for (const col of columns) {\n if (col.field && col.filterValue) {\n if (!map) map = new Map();\n map.set(col.field, col.filterValue);\n }\n }\n return map;\n }\n\n // #endregion\n\n // #region Internal State\n private filters: Map<string, FilterModel> = new Map();\n private cachedResult: unknown[] | null = null;\n private cacheKey: string | null = null;\n /** Spot-check of input rows for cache invalidation when upstream plugins (e.g. sort) change row order */\n private cachedInputSpot: { len: number; first: unknown; mid: unknown; last: unknown } | null = null;\n private openPanelField: string | null = null;\n private panelElement: HTMLElement | null = null;\n private panelAnchorElement: HTMLElement | null = null; // For CSS anchor positioning cleanup\n private searchText: Map<string, string> = new Map();\n private excludedValues: Map<string, Set<unknown>> = new Map();\n private panelAbortController: AbortController | null = null; // For panel-scoped listeners\n private globalStylesInjected = false;\n\n // Virtualization constants for filter value list\n private static readonly DEFAULT_LIST_ITEM_HEIGHT = 28;\n private static readonly LIST_OVERSCAN = 3;\n private static readonly LIST_BYPASS_THRESHOLD = 50; // Don't virtualize if < 50 items\n\n /**\n * Get the item height from CSS variable or fallback to default.\n * Reads --tbw-filter-item-height from the panel element.\n */\n private getListItemHeight(): number {\n if (this.panelElement) {\n const cssValue = getComputedStyle(this.panelElement).getPropertyValue('--tbw-filter-item-height');\n if (cssValue && cssValue.trim()) {\n const parsed = parseFloat(cssValue);\n if (!isNaN(parsed) && parsed > 0) {\n return parsed;\n }\n }\n }\n return FilteringPlugin.DEFAULT_LIST_ITEM_HEIGHT;\n }\n\n /**\n * Compute the inclusion (selected) map from the current filters and excluded values.\n * For set filters this is: uniqueValues \\ excludedValues.\n * Only includes entries for fields that have a set filter.\n * Uses a single-pass batch extraction to avoid iterating sourceRows per field.\n */\n private computeSelected(): Record<string, unknown[]> {\n // Collect the fields that need unique values\n const setFields: {\n field: string;\n filterValue?: (value: unknown, row: Record<string, unknown>) => unknown | unknown[];\n }[] = [];\n for (const [field, filter] of this.filters) {\n if (filter.type !== 'set' || filter.operator !== 'notIn') continue;\n const col = this.grid.effectiveConfig?.columns?.find((c) => c.field === field);\n setFields.push({ field, filterValue: col?.filterValue });\n }\n if (setFields.length === 0) return {};\n\n // Single pass through sourceRows for all fields\n const uniqueMap = getUniqueValuesBatch(this.sourceRows as Record<string, unknown>[], setFields);\n\n const selected: Record<string, unknown[]> = {};\n for (const { field } of setFields) {\n const excluded = this.excludedValues.get(field);\n const unique = uniqueMap.get(field) ?? [];\n selected[field] = excluded ? unique.filter((v) => !excluded.has(v)) : unique;\n }\n return selected;\n }\n\n /**\n * Sync excludedValues map from a filter model (for set filters).\n */\n private syncExcludedValues(field: string, filter: FilterModel | null): void {\n if (!filter) {\n this.excludedValues.delete(field);\n } else if (filter.type === 'set' && filter.operator === 'notIn' && Array.isArray(filter.value)) {\n this.excludedValues.set(field, new Set(filter.value));\n } else if (filter.type === 'set') {\n // Other set operators may have different semantics; clear for safety\n this.excludedValues.delete(field);\n }\n }\n // #endregion\n\n // #region Lifecycle\n\n /** @internal */\n override attach(grid: GridElement): void {\n super.attach(grid);\n this.injectGlobalStyles();\n }\n\n /** @internal */\n override detach(): void {\n this.filters.clear();\n this.cachedResult = null;\n this.cacheKey = null;\n this.cachedInputSpot = null;\n this.openPanelField = null;\n if (this.panelElement) {\n this.panelElement.remove();\n this.panelElement = null;\n }\n this.searchText.clear();\n this.excludedValues.clear();\n // Abort panel-scoped listeners (document click handler, etc.)\n this.panelAbortController?.abort();\n this.panelAbortController = null;\n }\n // #endregion\n\n // #region Query Handlers\n\n /**\n * Handle inter-plugin queries.\n * Contributes filter-related items to the header context menu.\n * @internal\n */\n override handleQuery(query: PluginQuery): unknown {\n if (query.type === 'getContextMenuItems') {\n const params = query.context as ContextMenuParams;\n if (!params.isHeader) return undefined;\n\n const column = params.column as ColumnConfig;\n if (!column?.field) return undefined;\n\n // Only contribute items if filtering is enabled for this column\n if (!this.isFilteringEnabled()) return undefined;\n if (!this.isColumnFilterable(column)) return undefined;\n\n const items: HeaderContextMenuItem[] = [];\n const fieldFiltered = this.isFieldFiltered(column.field);\n const hasAnyFilter = this.filters.size > 0;\n\n if (fieldFiltered) {\n items.push({\n id: 'filtering/clear-column-filter',\n label: `Clear Filter`,\n icon: '✕',\n order: 20,\n action: () => this.clearFieldFilter(column.field),\n });\n }\n\n if (hasAnyFilter) {\n items.push({\n id: 'filtering/clear-all-filters',\n label: 'Clear All Filters',\n icon: '✕',\n order: 21,\n disabled: !hasAnyFilter,\n action: () => this.clearAllFilters(),\n });\n }\n\n return items.length > 0 ? items : undefined;\n }\n return undefined;\n }\n // #endregion\n\n // #region Hooks\n\n /** @internal */\n override processRows(rows: readonly unknown[]): unknown[] {\n const filterList = [...this.filters.values()];\n if (!filterList.length) return [...rows];\n\n // If using async filterHandler, processRows becomes a passthrough\n // Actual filtering happens in applyFiltersAsync and rows are set directly on grid\n if (this.config.filterHandler) {\n // Return cached result if available (set by async handler)\n if (this.cachedResult) return this.cachedResult;\n // Otherwise return rows as-is (filtering happens async)\n return [...rows];\n }\n\n // Check cache — also verify input rows haven't changed (e.g. due to sort)\n const newCacheKey = computeFilterCacheKey(filterList);\n const inputSpot = {\n len: rows.length,\n first: rows[0],\n mid: rows[Math.floor(rows.length / 2)],\n last: rows[rows.length - 1],\n };\n const inputUnchanged =\n this.cachedInputSpot != null &&\n inputSpot.len === this.cachedInputSpot.len &&\n inputSpot.first === this.cachedInputSpot.first &&\n inputSpot.mid === this.cachedInputSpot.mid &&\n inputSpot.last === this.cachedInputSpot.last;\n\n if (this.cacheKey === newCacheKey && this.cachedResult && inputUnchanged) {\n return this.cachedResult;\n }\n\n // Filter rows synchronously (worker support can be added later)\n const result = filterRows(\n [...rows] as Record<string, unknown>[],\n filterList,\n this.config.caseSensitive,\n this.getFilterValues(),\n );\n\n // Update cache\n this.cachedResult = result;\n this.cacheKey = newCacheKey;\n this.cachedInputSpot = inputSpot;\n\n return result;\n }\n\n /** @internal */\n override afterRender(): void {\n const gridEl = this.gridElement;\n if (!gridEl) return;\n\n // Find all header cells (using part attribute, not class)\n const headerCells = gridEl.querySelectorAll('[part~=\"header-cell\"]');\n headerCells.forEach((cell) => {\n const colIndex = cell.getAttribute('data-col');\n if (colIndex === null) return;\n\n // Use visibleColumns since data-col is the index within _visibleColumns\n const col = this.visibleColumns[parseInt(colIndex, 10)] as ColumnConfig;\n if (!col || !this.isColumnFilterable(col)) return;\n\n // Skip utility columns (expander, selection checkbox, etc.)\n if (isUtilityColumn(col)) return;\n\n const field = col.field;\n if (!field) return;\n\n const hasFilter = this.filters.has(field);\n\n // Check if button already exists\n let filterBtn = cell.querySelector('.tbw-filter-btn') as HTMLElement | null;\n\n if (filterBtn) {\n // Update active state and icon of existing button\n const wasActive = filterBtn.classList.contains('active');\n filterBtn.classList.toggle('active', hasFilter);\n (cell as HTMLElement).classList.toggle('filtered', hasFilter);\n // Update icon if active state changed\n if (wasActive !== hasFilter) {\n const iconName = hasFilter ? 'filterActive' : 'filter';\n this.setIcon(filterBtn, this.resolveIcon(iconName));\n }\n return;\n }\n\n // Create filter button\n filterBtn = document.createElement('button');\n filterBtn.className = 'tbw-filter-btn';\n filterBtn.setAttribute('aria-label', `Filter ${col.header ?? field}`);\n // Use grid icons configuration\n const iconName = hasFilter ? 'filterActive' : 'filter';\n this.setIcon(filterBtn, this.resolveIcon(iconName));\n\n // Mark button as active if filter exists\n if (hasFilter) {\n filterBtn.classList.add('active');\n (cell as HTMLElement).classList.add('filtered');\n }\n\n filterBtn.addEventListener('click', (e) => {\n e.stopPropagation();\n this.toggleFilterPanel(field, col, filterBtn!);\n });\n\n // Insert before resize handle to maintain order: [label, sort-indicator, filter-btn, resize-handle]\n const resizeHandle = cell.querySelector('.resize-handle');\n if (resizeHandle) {\n cell.insertBefore(filterBtn, resizeHandle);\n } else {\n cell.appendChild(filterBtn);\n }\n });\n }\n // #endregion\n\n // #region Public API\n\n /**\n * Set a filter on a specific field.\n * Pass null to remove the filter.\n */\n setFilter(field: string, filter: Omit<FilterModel, 'field'> | null): void {\n if (filter === null) {\n this.filters.delete(field);\n this.syncExcludedValues(field, null);\n } else {\n const fullFilter = { ...filter, field };\n this.filters.set(field, fullFilter);\n this.syncExcludedValues(field, fullFilter);\n }\n // Invalidate cache\n this.cachedResult = null;\n this.cacheKey = null;\n this.cachedInputSpot = null;\n\n this.emit<FilterChangeDetail>('filter-change', {\n filters: [...this.filters.values()],\n filteredRowCount: 0, // Will be accurate after processRows\n selected: this.computeSelected(),\n });\n // Notify other plugins via Event Bus\n this.emitPluginEvent('filter-applied', { filters: [...this.filters.values()] });\n this.requestRender();\n }\n\n /**\n * Get the current filter for a field.\n */\n getFilter(field: string): FilterModel | undefined {\n return this.filters.get(field);\n }\n\n /**\n * Get all active filters.\n */\n getFilters(): FilterModel[] {\n return [...this.filters.values()];\n }\n\n /**\n * Alias for getFilters() to match functional API naming.\n */\n getFilterModel(): FilterModel[] {\n return this.getFilters();\n }\n\n /**\n * Set filters from an array (replaces all existing filters).\n */\n setFilterModel(filters: FilterModel[]): void {\n this.filters.clear();\n this.excludedValues.clear();\n for (const filter of filters) {\n this.filters.set(filter.field, filter);\n this.syncExcludedValues(filter.field, filter);\n }\n this.cachedResult = null;\n this.cacheKey = null;\n this.cachedInputSpot = null;\n\n this.emit<FilterChangeDetail>('filter-change', {\n filters: [...this.filters.values()],\n filteredRowCount: 0,\n selected: this.computeSelected(),\n });\n // Notify other plugins via Event Bus\n this.emitPluginEvent('filter-applied', { filters: [...this.filters.values()] });\n this.requestRender();\n }\n\n /**\n * Clear all filters.\n */\n clearAllFilters(): void {\n this.filters.clear();\n this.excludedValues.clear();\n this.searchText.clear();\n\n this.applyFiltersInternal();\n }\n\n /**\n * Clear filter for a specific field.\n */\n clearFieldFilter(field: string): void {\n this.filters.delete(field);\n this.excludedValues.delete(field);\n this.searchText.delete(field);\n\n this.applyFiltersInternal();\n }\n\n /**\n * Check if a field has an active filter.\n */\n isFieldFiltered(field: string): boolean {\n return this.filters.has(field);\n }\n\n /**\n * Get the count of filtered rows (from cache).\n */\n getFilteredRowCount(): number {\n return this.cachedResult?.length ?? this.rows.length;\n }\n\n /**\n * Get all active filters (alias for getFilters).\n */\n getActiveFilters(): FilterModel[] {\n return this.getFilters();\n }\n\n /**\n * Get unique values for a field (for set filter dropdowns).\n * Uses sourceRows to include all values regardless of current filter.\n * When a column has `filterValue`, individual extracted values are returned.\n */\n getUniqueValues(field: string): unknown[] {\n const col = this.grid.effectiveConfig?.columns?.find((c) => c.field === field);\n const getter = col?.filterValue;\n return getUniqueValues(this.sourceRows as Record<string, unknown>[], field, getter);\n }\n // #endregion\n\n // #region Private Methods\n\n /**\n * Copy CSS classes and data attributes from grid to filter panel.\n * This ensures theme classes (e.g., .eds-theme) cascade to the panel.\n */\n private copyGridThemeContext(panel: HTMLElement): void {\n const gridEl = this.gridElement;\n if (!gridEl) return;\n\n // Copy all CSS classes from grid to panel (except internal ones)\n for (const className of gridEl.classList) {\n // Skip internal classes that shouldn't be copied\n if (className.startsWith('tbw-') || className === 'selecting') continue;\n panel.classList.add(className);\n }\n\n // Copy data-theme attribute if present\n const theme = gridEl.dataset.theme;\n if (theme) {\n panel.dataset.theme = theme;\n }\n }\n\n /**\n * Inject global styles for filter panel (rendered in document.body)\n */\n private injectGlobalStyles(): void {\n if (this.globalStylesInjected) return;\n if (document.getElementById('tbw-filter-panel-styles')) {\n this.globalStylesInjected = true;\n return;\n }\n // Only inject if we have valid CSS text (Vite's ?inline import)\n // When importing from source without Vite, the import is a module object, not a string\n if (typeof filterPanelStyles !== 'string' || !filterPanelStyles) {\n this.globalStylesInjected = true;\n return;\n }\n const style = document.createElement('style');\n style.id = 'tbw-filter-panel-styles';\n style.textContent = filterPanelStyles;\n document.head.appendChild(style);\n this.globalStylesInjected = true;\n }\n\n /**\n * Toggle the filter panel for a field\n */\n private toggleFilterPanel(field: string, column: ColumnConfig, buttonEl: HTMLElement): void {\n // Close if already open\n if (this.openPanelField === field) {\n this.closeFilterPanel();\n return;\n }\n\n // Close any existing panel\n this.closeFilterPanel();\n\n // Create panel\n const panel = document.createElement('div');\n panel.className = 'tbw-filter-panel';\n // Copy theme classes from grid to panel for proper theming\n this.copyGridThemeContext(panel);\n // Add animation class if animations are enabled\n if (this.isAnimationEnabled) {\n panel.classList.add('tbw-filter-panel-animated');\n }\n this.panelElement = panel;\n this.openPanelField = field;\n\n // If using async valuesHandler, show loading state and fetch values\n if (this.config.valuesHandler) {\n panel.innerHTML = '<div class=\"tbw-filter-loading\">Loading...</div>';\n document.body.appendChild(panel);\n this.positionPanel(panel, buttonEl);\n this.setupPanelCloseHandler(panel, buttonEl);\n\n this.config.valuesHandler(field, column).then((values) => {\n // Check if panel is still open for this field\n if (this.openPanelField !== field || !this.panelElement) return;\n panel.innerHTML = '';\n this.renderPanelContent(field, column, panel, values);\n });\n return;\n }\n\n // Sync path: get unique values from local rows\n const uniqueValues = getUniqueValues(this.sourceRows as Record<string, unknown>[], field, column.filterValue);\n\n // Position and append to body BEFORE rendering content\n // so getListItemHeight() can read CSS variables from computed styles\n document.body.appendChild(panel);\n this.positionPanel(panel, buttonEl);\n\n this.renderPanelContent(field, column, panel, uniqueValues);\n this.setupPanelCloseHandler(panel, buttonEl);\n }\n\n /**\n * Render filter panel content with given values\n */\n private renderPanelContent(field: string, column: ColumnConfig, panel: HTMLElement, uniqueValues: unknown[]): void {\n // Get current excluded values or initialize empty\n let excludedSet = this.excludedValues.get(field);\n if (!excludedSet) {\n excludedSet = new Set();\n this.excludedValues.set(field, excludedSet);\n }\n\n // Get current search text\n const currentSearchText = this.searchText.get(field) ?? '';\n\n // Create panel params for custom renderer\n const params: FilterPanelParams = {\n field,\n column,\n uniqueValues,\n excludedValues: excludedSet,\n searchText: currentSearchText,\n applySetFilter: (excluded: unknown[]) => {\n this.applySetFilter(field, excluded);\n this.closeFilterPanel();\n },\n applyTextFilter: (operator, value, valueTo) => {\n this.applyTextFilter(field, operator, value, valueTo);\n this.closeFilterPanel();\n },\n clearFilter: () => {\n this.clearFieldFilter(field);\n this.closeFilterPanel();\n },\n closePanel: () => this.closeFilterPanel(),\n };\n\n // Use custom renderer or default\n // Custom renderer can return undefined to fall back to default panel for specific columns\n // Resolution order: plugin config → typeDefaults → built-in\n let usedCustomRenderer = false;\n\n // 1. Check plugin-level filterPanelRenderer\n if (this.config.filterPanelRenderer) {\n this.config.filterPanelRenderer(panel, params);\n // If renderer added content to panel, it handled rendering\n usedCustomRenderer = panel.children.length > 0;\n }\n\n // 2. Check typeDefaults for this column's type\n if (!usedCustomRenderer && column.type) {\n const typeDefault = this.grid.effectiveConfig.typeDefaults?.[column.type];\n if (typeDefault?.filterPanelRenderer) {\n typeDefault.filterPanelRenderer(panel, params);\n usedCustomRenderer = panel.children.length > 0;\n }\n }\n\n // 3. Fall back to built-in type-specific panel renderers\n if (!usedCustomRenderer) {\n const columnType = column.type;\n if (columnType === 'number') {\n this.renderNumberFilterPanel(panel, params, uniqueValues);\n } else if (columnType === 'date') {\n this.renderDateFilterPanel(panel, params, uniqueValues);\n } else {\n this.renderDefaultFilterPanel(panel, params, uniqueValues, excludedSet);\n }\n }\n }\n\n /**\n * Setup click-outside handler to close the panel\n */\n private setupPanelCloseHandler(panel: HTMLElement, buttonEl: HTMLElement): void {\n // Create abort controller for panel-scoped listeners\n // This allows cleanup when panel closes OR when grid disconnects\n this.panelAbortController = new AbortController();\n\n // Add global click handler to close on outside click\n // Defer to next tick to avoid immediate close from the click that opened the panel\n setTimeout(() => {\n document.addEventListener(\n 'click',\n (e: MouseEvent) => {\n if (!panel.contains(e.target as Node) && e.target !== buttonEl) {\n this.closeFilterPanel();\n }\n },\n { signal: this.panelAbortController?.signal },\n );\n }, 0);\n }\n\n /**\n * Close the filter panel\n */\n private closeFilterPanel(): void {\n const panel = this.panelElement;\n if (panel) {\n panel.remove();\n this.panelElement = null;\n }\n // Clean up anchor name from header cell\n if (this.panelAnchorElement) {\n (this.panelAnchorElement.style as any).anchorName = '';\n this.panelAnchorElement = null;\n }\n this.openPanelField = null;\n // Abort panel-scoped listeners (document click handler)\n this.panelAbortController?.abort();\n this.panelAbortController = null;\n }\n\n /** Cache for CSS anchor positioning support check */\n private static supportsAnchorPositioning: boolean | null = null;\n\n /**\n * Check if browser supports CSS Anchor Positioning\n */\n private static checkAnchorPositioningSupport(): boolean {\n if (FilteringPlugin.supportsAnchorPositioning === null) {\n FilteringPlugin.supportsAnchorPositioning = CSS.supports('anchor-name', '--test');\n }\n return FilteringPlugin.supportsAnchorPositioning;\n }\n\n /**\n * Position the panel below the header cell\n * Uses CSS Anchor Positioning if supported, falls back to JS positioning\n */\n private positionPanel(panel: HTMLElement, buttonEl: HTMLElement): void {\n // Find the parent header cell\n const headerCell = buttonEl.closest('.cell') as HTMLElement | null;\n const anchorEl = headerCell ?? buttonEl;\n\n // Set anchor name on the header cell for CSS anchor positioning\n (anchorEl.style as any).anchorName = '--tbw-filter-anchor';\n this.panelAnchorElement = anchorEl; // Store for cleanup\n\n // If CSS Anchor Positioning is supported, CSS handles positioning\n // but we need to detect if it flipped above to adjust animation\n if (FilteringPlugin.checkAnchorPositioningSupport()) {\n // Check position after CSS anchor positioning takes effect\n requestAnimationFrame(() => {\n const panelRect = panel.getBoundingClientRect();\n const anchorRect = anchorEl.getBoundingClientRect();\n // If panel top is above anchor top, it flipped to above\n if (panelRect.top < anchorRect.top) {\n panel.classList.add('tbw-filter-panel-above');\n }\n });\n return;\n }\n\n // Fallback: JS-based positioning for older browsers\n const rect = anchorEl.getBoundingClientRect();\n\n panel.style.position = 'fixed';\n panel.style.top = `${rect.bottom + 4}px`;\n panel.style.left = `${rect.left}px`;\n\n // Adjust if overflows viewport edges\n requestAnimationFrame(() => {\n const panelRect = panel.getBoundingClientRect();\n\n // Check horizontal overflow - align right edge to header cell right edge\n if (panelRect.right > window.innerWidth - 8) {\n panel.style.left = `${rect.right - panelRect.width}px`;\n }\n\n // Check vertical overflow - flip to above header cell\n if (panelRect.bottom > window.innerHeight - 8) {\n panel.style.top = `${rect.top - panelRect.height - 4}px`;\n panel.classList.add('tbw-filter-panel-above');\n }\n });\n }\n\n /**\n * Render the default filter panel content\n */\n private renderDefaultFilterPanel(\n panel: HTMLElement,\n params: FilterPanelParams,\n uniqueValues: unknown[],\n excludedValues: Set<unknown>,\n ): void {\n const { field, column } = params;\n // Get item height from CSS variable or use default\n const itemHeight = this.getListItemHeight();\n\n // Helper: format a value using the column's format function (for ID-to-name translation, etc.)\n // When filterValue is set, unique values are already extracted primitives — skip format.\n const formatValue = (value: unknown): string => {\n if (value == null) return '(Blank)';\n if (column.format && !column.filterValue) {\n const formatted = column.format(value, undefined as never);\n if (formatted) return formatted;\n }\n return String(value);\n };\n\n // Sort unique values by formatted display name\n uniqueValues = uniqueValues.slice().sort((a, b) => formatValue(a).localeCompare(formatValue(b)));\n\n // Search input\n const searchContainer = document.createElement('div');\n searchContainer.className = 'tbw-filter-search';\n\n const searchInput = document.createElement('input');\n searchInput.type = 'text';\n searchInput.placeholder = 'Search...';\n searchInput.className = 'tbw-filter-search-input';\n searchInput.value = this.searchText.get(field) ?? '';\n searchContainer.appendChild(searchInput);\n panel.appendChild(searchContainer);\n\n // Select All tristate checkbox\n const actionsRow = document.createElement('div');\n actionsRow.className = 'tbw-filter-actions';\n\n const selectAllLabel = document.createElement('label');\n selectAllLabel.className = 'tbw-filter-value-item';\n selectAllLabel.style.padding = '0';\n selectAllLabel.style.margin = '0';\n\n const selectAllCheckbox = document.createElement('input');\n selectAllCheckbox.type = 'checkbox';\n selectAllCheckbox.className = 'tbw-filter-checkbox';\n\n const selectAllText = document.createElement('span');\n selectAllText.textContent = 'Select All';\n\n selectAllLabel.appendChild(selectAllCheckbox);\n selectAllLabel.appendChild(selectAllText);\n actionsRow.appendChild(selectAllLabel);\n\n // Update tristate checkbox based on checkState\n const updateSelectAllState = () => {\n const values = [...checkState.values()];\n const allChecked = values.every((v) => v);\n const noneChecked = values.every((v) => !v);\n\n selectAllCheckbox.checked = allChecked;\n selectAllCheckbox.indeterminate = !allChecked && !noneChecked;\n };\n\n // Toggle all on click\n selectAllCheckbox.addEventListener('change', () => {\n const newState = selectAllCheckbox.checked;\n for (const key of checkState.keys()) {\n checkState.set(key, newState);\n }\n updateSelectAllState();\n renderVisibleItems();\n });\n\n panel.appendChild(actionsRow);\n\n // Values container with virtualization support\n const valuesContainer = document.createElement('div');\n valuesContainer.className = 'tbw-filter-values';\n\n // Spacer for virtual height\n const spacer = document.createElement('div');\n spacer.className = 'tbw-filter-values-spacer';\n valuesContainer.appendChild(spacer);\n\n // Content container positioned absolutely\n const contentContainer = document.createElement('div');\n contentContainer.className = 'tbw-filter-values-content';\n valuesContainer.appendChild(contentContainer);\n\n // Track current check state for values (persists across virtualizations)\n const checkState = new Map<string, boolean>();\n uniqueValues.forEach((value) => {\n const key = value == null ? '__null__' : String(value);\n checkState.set(key, !excludedValues.has(value));\n });\n\n // Initialize select all state\n updateSelectAllState();\n\n // Filtered values cache\n let filteredValues: unknown[] = [];\n\n // Create a single checkbox item element\n const createItem = (value: unknown, index: number): HTMLElement => {\n const displayValue = formatValue(value);\n const key = value == null ? '__null__' : String(value);\n\n const item = document.createElement('label');\n item.className = 'tbw-filter-value-item';\n item.style.position = 'absolute';\n item.style.top = `calc(var(--tbw-filter-item-height, 28px) * ${index})`;\n item.style.left = '0';\n item.style.right = '0';\n item.style.boxSizing = 'border-box';\n\n const checkbox = document.createElement('input');\n checkbox.type = 'checkbox';\n checkbox.className = 'tbw-filter-checkbox';\n checkbox.checked = checkState.get(key) ?? true;\n checkbox.dataset.value = key;\n\n // Sync check state on change and update tristate checkbox\n checkbox.addEventListener('change', () => {\n checkState.set(key, checkbox.checked);\n updateSelectAllState();\n });\n\n const label = document.createElement('span');\n label.textContent = displayValue;\n\n item.appendChild(checkbox);\n item.appendChild(label);\n return item;\n };\n\n // Render visible items using virtualization\n const renderVisibleItems = () => {\n const totalItems = filteredValues.length;\n const viewportHeight = valuesContainer.clientHeight;\n const scrollTop = valuesContainer.scrollTop;\n\n // Set total height for scrollbar\n spacer.style.height = `${totalItems * itemHeight}px`;\n\n // Bypass virtualization for small lists\n if (shouldBypassVirtualization(totalItems, FilteringPlugin.LIST_BYPASS_THRESHOLD / 3)) {\n contentContainer.innerHTML = '';\n contentContainer.style.transform = 'translateY(0px)';\n filteredValues.forEach((value, idx) => {\n contentContainer.appendChild(createItem(value, idx));\n });\n return;\n }\n\n // Use computeVirtualWindow for real-scroll virtualization\n const window = computeVirtualWindow({\n totalRows: totalItems,\n viewportHeight,\n scrollTop,\n rowHeight: itemHeight,\n overscan: FilteringPlugin.LIST_OVERSCAN,\n });\n\n // Position content container\n contentContainer.style.transform = `translateY(${window.offsetY}px)`;\n\n // Clear and render visible items\n contentContainer.innerHTML = '';\n for (let i = window.start; i < window.end; i++) {\n contentContainer.appendChild(createItem(filteredValues[i], i - window.start));\n }\n };\n\n // Filter and re-render values\n const renderValues = (filterText: string) => {\n const caseSensitive = this.config.caseSensitive ?? false;\n const compareFilter = caseSensitive ? filterText : filterText.toLowerCase();\n\n // Filter the unique values - search against formatted display name\n filteredValues = uniqueValues.filter((value) => {\n const displayStr = formatValue(value);\n const compareValue = caseSensitive ? displayStr : displayStr.toLowerCase();\n return !filterText || compareValue.includes(compareFilter);\n });\n\n if (filteredValues.length === 0) {\n spacer.style.height = '0px';\n contentContainer.innerHTML = '';\n const noMatch = document.createElement('div');\n noMatch.className = 'tbw-filter-no-match';\n noMatch.textContent = 'No matching values';\n contentContainer.appendChild(noMatch);\n return;\n }\n\n renderVisibleItems();\n };\n\n // Scroll handler for virtualization\n valuesContainer.addEventListener(\n 'scroll',\n () => {\n if (filteredValues.length > 0) {\n renderVisibleItems();\n }\n },\n { passive: true },\n );\n\n renderValues(searchInput.value);\n panel.appendChild(valuesContainer);\n\n // Debounced search\n let debounceTimer: ReturnType<typeof setTimeout>;\n searchInput.addEventListener('input', () => {\n clearTimeout(debounceTimer);\n debounceTimer = setTimeout(() => {\n this.searchText.set(field, searchInput.value);\n renderValues(searchInput.value);\n }, this.config.debounceMs ?? 150);\n });\n\n // Apply/Clear buttons\n const buttonRow = document.createElement('div');\n buttonRow.className = 'tbw-filter-buttons';\n\n const applyBtn = document.createElement('button');\n applyBtn.className = 'tbw-filter-apply-btn';\n applyBtn.textContent = 'Apply';\n applyBtn.addEventListener('click', () => {\n // Read from checkState map (works with virtualization)\n const excluded: unknown[] = [];\n for (const [key, isChecked] of checkState) {\n if (!isChecked) {\n if (key === '__null__') {\n excluded.push(null);\n } else {\n // Try to match original value type\n const original = uniqueValues.find((v) => String(v) === key);\n excluded.push(original !== undefined ? original : key);\n }\n }\n }\n params.applySetFilter(excluded);\n });\n buttonRow.appendChild(applyBtn);\n\n const clearBtn = document.createElement('button');\n clearBtn.className = 'tbw-filter-clear-btn';\n clearBtn.textContent = 'Clear Filter';\n clearBtn.addEventListener('click', () => {\n params.clearFilter();\n });\n buttonRow.appendChild(clearBtn);\n\n panel.appendChild(buttonRow);\n }\n\n /**\n * Render a number range filter panel with min/max inputs and slider\n */\n private renderNumberFilterPanel(panel: HTMLElement, params: FilterPanelParams, uniqueValues: unknown[]): void {\n const { field, column } = params;\n\n // Get range configuration from filterParams, editorParams, or compute from data\n const filterParams = column.filterParams;\n const editorParams = column.editorParams as { min?: number; max?: number; step?: number } | undefined;\n\n // Helper to convert to number\n const toNumber = (val: unknown, fallback: number): number => {\n if (typeof val === 'number') return val;\n if (typeof val === 'string') {\n const num = parseFloat(val);\n return isNaN(num) ? fallback : num;\n }\n return fallback;\n };\n\n // Compute min/max from data if not specified\n const numericValues = uniqueValues.filter((v) => typeof v === 'number' && !isNaN(v)) as number[];\n const dataMin = numericValues.length > 0 ? Math.min(...numericValues) : 0;\n const dataMax = numericValues.length > 0 ? Math.max(...numericValues) : 100;\n\n const min = toNumber(filterParams?.min ?? editorParams?.min, dataMin);\n const max = toNumber(filterParams?.max ?? editorParams?.max, dataMax);\n const step = filterParams?.step ?? editorParams?.step ?? 1;\n\n // Get current filter values if any\n const currentFilter = this.filters.get(field);\n let currentMin = min;\n let currentMax = max;\n if (currentFilter?.operator === 'between') {\n currentMin = toNumber(currentFilter.value, min);\n currentMax = toNumber(currentFilter.valueTo, max);\n } else if (currentFilter?.operator === 'greaterThanOrEqual') {\n currentMin = toNumber(currentFilter.value, min);\n } else if (currentFilter?.operator === 'lessThanOrEqual') {\n currentMax = toNumber(currentFilter.value, max);\n }\n\n // Range inputs container\n const rangeContainer = document.createElement('div');\n rangeContainer.className = 'tbw-filter-range-inputs';\n\n // Min input\n const minGroup = document.createElement('div');\n minGroup.className = 'tbw-filter-range-group';\n\n const minLabel = document.createElement('label');\n minLabel.textContent = 'Min';\n minLabel.className = 'tbw-filter-range-label';\n\n const minInput = document.createElement('input');\n minInput.type = 'number';\n minInput.className = 'tbw-filter-range-input';\n minInput.min = String(min);\n minInput.max = String(max);\n minInput.step = String(step);\n minInput.value = String(currentMin);\n\n minGroup.appendChild(minLabel);\n minGroup.appendChild(minInput);\n rangeContainer.appendChild(minGroup);\n\n // Separator\n const separator = document.createElement('span');\n separator.className = 'tbw-filter-range-separator';\n separator.textContent = '–';\n rangeContainer.appendChild(separator);\n\n // Max input\n const maxGroup = document.createElement('div');\n maxGroup.className = 'tbw-filter-range-group';\n\n const maxLabel = document.createElement('label');\n maxLabel.textContent = 'Max';\n maxLabel.className = 'tbw-filter-range-label';\n\n const maxInput = document.createElement('input');\n maxInput.type = 'number';\n maxInput.className = 'tbw-filter-range-input';\n maxInput.min = String(min);\n maxInput.max = String(max);\n maxInput.step = String(step);\n maxInput.value = String(currentMax);\n\n maxGroup.appendChild(maxLabel);\n maxGroup.appendChild(maxInput);\n rangeContainer.appendChild(maxGroup);\n\n panel.appendChild(rangeContainer);\n\n // Range slider (dual thumb using two range inputs)\n const sliderContainer = document.createElement('div');\n sliderContainer.className = 'tbw-filter-range-slider';\n\n const sliderTrack = document.createElement('div');\n sliderTrack.className = 'tbw-filter-range-track';\n\n const sliderFill = document.createElement('div');\n sliderFill.className = 'tbw-filter-range-fill';\n\n const minSlider = document.createElement('input');\n minSlider.type = 'range';\n minSlider.className = 'tbw-filter-range-thumb tbw-filter-range-thumb-min';\n minSlider.min = String(min);\n minSlider.max = String(max);\n minSlider.step = String(step);\n minSlider.value = String(currentMin);\n\n const maxSlider = document.createElement('input');\n maxSlider.type = 'range';\n maxSlider.className = 'tbw-filter-range-thumb tbw-filter-range-thumb-max';\n maxSlider.min = String(min);\n maxSlider.max = String(max);\n maxSlider.step = String(step);\n maxSlider.value = String(currentMax);\n\n sliderContainer.appendChild(sliderTrack);\n sliderContainer.appendChild(sliderFill);\n sliderContainer.appendChild(minSlider);\n sliderContainer.appendChild(maxSlider);\n panel.appendChild(sliderContainer);\n\n // Update fill position\n const updateFill = () => {\n const minVal = parseFloat(minSlider.value);\n const maxVal = parseFloat(maxSlider.value);\n const range = max - min;\n const leftPercent = ((minVal - min) / range) * 100;\n const rightPercent = ((maxVal - min) / range) * 100;\n sliderFill.style.left = `${leftPercent}%`;\n sliderFill.style.width = `${rightPercent - leftPercent}%`;\n };\n\n // Sync inputs with sliders\n minSlider.addEventListener('input', () => {\n const val = Math.min(parseFloat(minSlider.value), parseFloat(maxSlider.value));\n minSlider.value = String(val);\n minInput.value = String(val);\n updateFill();\n });\n\n maxSlider.addEventListener('input', () => {\n const val = Math.max(parseFloat(maxSlider.value), parseFloat(minSlider.value));\n maxSlider.value = String(val);\n maxInput.value = String(val);\n updateFill();\n });\n\n // Sync sliders with inputs\n minInput.addEventListener('input', () => {\n let val = parseFloat(minInput.value) || min;\n val = Math.max(min, Math.min(val, parseFloat(maxInput.value)));\n minSlider.value = String(val);\n updateFill();\n });\n\n maxInput.addEventListener('input', () => {\n let val = parseFloat(maxInput.value) || max;\n val = Math.min(max, Math.max(val, parseFloat(minInput.value)));\n maxSlider.value = String(val);\n updateFill();\n });\n\n // Initialize fill\n updateFill();\n\n // Apply/Clear buttons\n const buttonRow = document.createElement('div');\n buttonRow.className = 'tbw-filter-buttons';\n\n const applyBtn = document.createElement('button');\n applyBtn.className = 'tbw-filter-apply-btn';\n applyBtn.textContent = 'Apply';\n applyBtn.addEventListener('click', () => {\n const minVal = parseFloat(minInput.value);\n const maxVal = parseFloat(maxInput.value);\n params.applyTextFilter('between', minVal, maxVal);\n });\n buttonRow.appendChild(applyBtn);\n\n const clearBtn = document.createElement('button');\n clearBtn.className = 'tbw-filter-clear-btn';\n clearBtn.textContent = 'Clear Filter';\n clearBtn.addEventListener('click', () => {\n params.clearFilter();\n });\n buttonRow.appendChild(clearBtn);\n\n panel.appendChild(buttonRow);\n }\n\n /**\n * Render a date range filter panel with from/to date inputs\n */\n private renderDateFilterPanel(panel: HTMLElement, params: FilterPanelParams, uniqueValues: unknown[]): void {\n const { field, column } = params;\n\n // Get range configuration from filterParams, editorParams, or compute from data\n const filterParams = column.filterParams;\n const editorParams = column.editorParams as { min?: string; max?: string } | undefined;\n\n // Compute min/max from data if not specified\n const dateValues = uniqueValues\n .filter((v) => v instanceof Date || (typeof v === 'string' && !isNaN(Date.parse(v))))\n .map((v) => (v instanceof Date ? v : new Date(v as string)))\n .filter((d) => !isNaN(d.getTime()));\n\n const dataMin = dateValues.length > 0 ? new Date(Math.min(...dateValues.map((d) => d.getTime()))) : null;\n const dataMax = dateValues.length > 0 ? new Date(Math.max(...dateValues.map((d) => d.getTime()))) : null;\n\n // Format date for input[type=\"date\"] (YYYY-MM-DD)\n const formatDateForInput = (date: Date | null): string => {\n if (!date) return '';\n return date.toISOString().split('T')[0];\n };\n\n const parseFilterParam = (value: unknown): string => {\n if (!value) return '';\n if (typeof value === 'string') return value;\n if (typeof value === 'number') return formatDateForInput(new Date(value));\n return '';\n };\n\n const minDate =\n parseFilterParam(filterParams?.min) || parseFilterParam(editorParams?.min) || formatDateForInput(dataMin);\n const maxDate =\n parseFilterParam(filterParams?.max) || parseFilterParam(editorParams?.max) || formatDateForInput(dataMax);\n\n // Get current filter values if any\n const currentFilter = this.filters.get(field);\n let currentFrom = '';\n let currentTo = '';\n const isBlankFilter = currentFilter?.operator === 'blank';\n if (currentFilter?.operator === 'between') {\n currentFrom = parseFilterParam(currentFilter.value) || '';\n currentTo = parseFilterParam(currentFilter.valueTo) || '';\n } else if (currentFilter?.operator === 'greaterThanOrEqual') {\n currentFrom = parseFilterParam(currentFilter.value) || '';\n } else if (currentFilter?.operator === 'lessThanOrEqual') {\n currentTo = parseFilterParam(currentFilter.value) || '';\n }\n\n // Date range inputs container\n const rangeContainer = document.createElement('div');\n rangeContainer.className = 'tbw-filter-date-range';\n\n // From input\n const fromGroup = document.createElement('div');\n fromGroup.className = 'tbw-filter-date-group';\n\n const fromLabel = document.createElement('label');\n fromLabel.textContent = 'From';\n fromLabel.className = 'tbw-filter-range-label';\n\n const fromInput = document.createElement('input');\n fromInput.type = 'date';\n fromInput.className = 'tbw-filter-date-input';\n if (minDate) fromInput.min = minDate;\n if (maxDate) fromInput.max = maxDate;\n fromInput.value = currentFrom;\n\n fromGroup.appendChild(fromLabel);\n fromGroup.appendChild(fromInput);\n rangeContainer.appendChild(fromGroup);\n\n // Separator\n const separator = document.createElement('span');\n separator.className = 'tbw-filter-range-separator';\n separator.textContent = '–';\n rangeContainer.appendChild(separator);\n\n // To input\n const toGroup = document.createElement('div');\n toGroup.className = 'tbw-filter-date-group';\n\n const toLabel = document.createElement('label');\n toLabel.textContent = 'To';\n toLabel.className = 'tbw-filter-range-label';\n\n const toInput = document.createElement('input');\n toInput.type = 'date';\n toInput.className = 'tbw-filter-date-input';\n if (minDate) toInput.min = minDate;\n if (maxDate) toInput.max = maxDate;\n toInput.value = currentTo;\n\n toGroup.appendChild(toLabel);\n toGroup.appendChild(toInput);\n rangeContainer.appendChild(toGroup);\n\n panel.appendChild(rangeContainer);\n\n // \"Show only blank\" checkbox\n const blankRow = document.createElement('label');\n blankRow.className = 'tbw-filter-blank-option';\n\n const blankCheckbox = document.createElement('input');\n blankCheckbox.type = 'checkbox';\n blankCheckbox.className = 'tbw-filter-blank-checkbox';\n blankCheckbox.checked = isBlankFilter;\n\n const blankLabel = document.createTextNode('Show only blank');\n blankRow.appendChild(blankCheckbox);\n blankRow.appendChild(blankLabel);\n\n // Toggle date inputs disabled state when blank is checked\n const toggleDateInputs = (disabled: boolean): void => {\n fromInput.disabled = disabled;\n toInput.disabled = disabled;\n rangeContainer.classList.toggle('tbw-filter-disabled', disabled);\n };\n toggleDateInputs(isBlankFilter);\n\n blankCheckbox.addEventListener('change', () => {\n toggleDateInputs(blankCheckbox.checked);\n });\n\n panel.appendChild(blankRow);\n\n // Apply/Clear buttons\n const buttonRow = document.createElement('div');\n buttonRow.className = 'tbw-filter-buttons';\n\n const applyBtn = document.createElement('button');\n applyBtn.className = 'tbw-filter-apply-btn';\n applyBtn.textContent = 'Apply';\n applyBtn.addEventListener('click', () => {\n if (blankCheckbox.checked) {\n params.applyTextFilter('blank', '');\n return;\n }\n\n const from = fromInput.value;\n const to = toInput.value;\n\n if (from && to) {\n params.applyTextFilter('between', from, to);\n } else if (from) {\n params.applyTextFilter('greaterThanOrEqual', from);\n } else if (to) {\n params.applyTextFilter('lessThanOrEqual', to);\n } else {\n params.clearFilter();\n }\n });\n buttonRow.appendChild(applyBtn);\n\n const clearBtn = document.createElement('button');\n clearBtn.className = 'tbw-filter-clear-btn';\n clearBtn.textContent = 'Clear Filter';\n clearBtn.addEventListener('click', () => {\n params.clearFilter();\n });\n buttonRow.appendChild(clearBtn);\n\n panel.appendChild(buttonRow);\n }\n\n /**\n * Apply a set filter (exclude values)\n */\n private applySetFilter(field: string, excluded: unknown[]): void {\n // Store excluded values\n this.excludedValues.set(field, new Set(excluded));\n\n if (excluded.length === 0) {\n // No exclusions = no filter\n this.filters.delete(field);\n } else {\n // Create \"notIn\" filter\n this.filters.set(field, {\n field,\n type: 'set',\n operator: 'notIn',\n value: excluded,\n });\n }\n\n this.applyFiltersInternal();\n }\n\n /**\n * Apply a text/number/date filter\n */\n private applyTextFilter(\n field: string,\n operator: FilterModel['operator'],\n value: string | number,\n valueTo?: string | number,\n ): void {\n this.filters.set(field, {\n field,\n type: 'text',\n operator,\n value,\n valueTo,\n });\n\n this.applyFiltersInternal();\n }\n\n /**\n * Internal method to apply filters (sync or async based on config)\n */\n private applyFiltersInternal(): void {\n this.cachedResult = null;\n this.cacheKey = null;\n this.cachedInputSpot = null;\n\n const filterList = [...this.filters.values()];\n\n // If using async filterHandler, delegate to server\n if (this.config.filterHandler) {\n const gridEl = this.grid as unknown as Element;\n gridEl.setAttribute('aria-busy', 'true');\n\n const result = this.config.filterHandler(filterList, this.sourceRows as unknown[]);\n\n // Handle async or sync result\n const handleResult = (rows: unknown[]) => {\n gridEl.removeAttribute('aria-busy');\n this.cachedResult = rows;\n\n // Update grid rows directly for async filtering\n (this.grid as unknown as { rows: unknown[] }).rows = rows;\n\n this.emit<FilterChangeDetail>('filter-change', {\n filters: filterList,\n filteredRowCount: rows.length,\n selected: this.computeSelected(),\n });\n // Notify other plugins via Event Bus\n this.emitPluginEvent('filter-applied', { filters: filterList });\n\n // Trigger afterRender to update filter button active state\n this.requestRender();\n };\n\n if (result && typeof (result as Promise<unknown[]>).then === 'function') {\n (result as Promise<unknown[]>).then(handleResult);\n } else {\n handleResult(result as unknown[]);\n }\n return;\n }\n\n // Sync path: emit event and re-render (processRows will handle filtering)\n this.emit<FilterChangeDetail>('filter-change', {\n filters: filterList,\n filteredRowCount: 0,\n selected: this.computeSelected(),\n });\n // Notify other plugins via Event Bus\n this.emitPluginEvent('filter-applied', { filters: filterList });\n this.requestRender();\n }\n // #endregion\n\n // #region Column State Hooks\n\n /**\n * Return filter state for a column if it has an active filter.\n * @internal\n */\n override getColumnState(field: string): Partial<ColumnState> | undefined {\n const filterModel = this.filters.get(field);\n if (!filterModel) return undefined;\n\n return {\n filter: {\n type: filterModel.type,\n operator: filterModel.operator,\n value: filterModel.value,\n valueTo: filterModel.valueTo,\n },\n };\n }\n\n /**\n * Apply filter state from column state.\n * @internal\n */\n override applyColumnState(field: string, state: ColumnState): void {\n // Only process if the column has filter state\n if (!state.filter) {\n this.filters.delete(field);\n return;\n }\n\n // Reconstruct the FilterModel from the stored state\n const filterModel: FilterModel = {\n field,\n type: state.filter.type,\n operator: state.filter.operator as FilterModel['operator'],\n value: state.filter.value,\n valueTo: state.filter.valueTo,\n };\n\n this.filters.set(field, filterModel);\n // Invalidate cache so filter is reapplied\n this.cachedResult = null;\n this.cacheKey = null;\n this.cachedInputSpot = null;\n }\n // #endregion\n}\n"],"names":["BLANK_FILTER_VALUE","toNumeric","value","n","matchesFilter","row","filter","caseSensitive","filterValue","rawValue","extracted","values","excluded","v","included","stringValue","compareValue","compareFilterValue","filterRows","rows","filters","filterValues","f","computeFilterCacheKey","getUniqueValues","field","hasBlank","cellValue","a","b","getUniqueValuesBatch","fields","acc","entry","result","hasExtractor","computeVirtualWindow","params","totalRows","viewportHeight","scrollTop","rowHeight","overscan","visibleRows","start","end","shouldBypassVirtualization","threshold","DEFAULT_FILTER_ICON","DEFAULT_GRID_ICONS","BaseGridPlugin","#abortController","config","grid","PluginClass","eventName","detail","event","eventType","callback","userIcons","mode","host","durationStr","parsed","iconKey","pluginOverride","element","icon","message","isUtilityColumn","column","FilteringPlugin","styles","col","columns","map","cssValue","setFields","c","uniqueMap","selected","unique","query","items","fieldFiltered","hasAnyFilter","filterList","newCacheKey","inputSpot","inputUnchanged","gridEl","cell","colIndex","hasFilter","filterBtn","wasActive","iconName","e","resizeHandle","fullFilter","getter","panel","className","theme","style","filterPanelStyles","buttonEl","uniqueValues","excludedSet","currentSearchText","operator","valueTo","usedCustomRenderer","typeDefault","columnType","anchorEl","panelRect","anchorRect","rect","excludedValues","itemHeight","formatValue","formatted","searchContainer","searchInput","actionsRow","selectAllLabel","selectAllCheckbox","selectAllText","updateSelectAllState","checkState","allChecked","noneChecked","newState","key","renderVisibleItems","valuesContainer","spacer","contentContainer","filteredValues","createItem","index","displayValue","item","checkbox","label","totalItems","idx","window","i","renderValues","filterText","compareFilter","displayStr","noMatch","debounceTimer","buttonRow","applyBtn","isChecked","original","clearBtn","filterParams","editorParams","toNumber","val","fallback","num","numericValues","dataMin","dataMax","min","max","step","currentFilter","currentMin","currentMax","rangeContainer","minGroup","minLabel","minInput","separator","maxGroup","maxLabel","maxInput","sliderContainer","sliderTrack","sliderFill","minSlider","maxSlider","updateFill","minVal","maxVal","range","leftPercent","rightPercent","dateValues","d","formatDateForInput","date","parseFilterParam","minDate","maxDate","currentFrom","currentTo","isBlankFilter","fromGroup","fromLabel","fromInput","toGroup","toLabel","toInput","blankRow","blankCheckbox","blankLabel","toggleDateInputs","disabled","from","to","handleResult","filterModel","state"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sources":["../../../../../../libs/grid/src/lib/plugins/filtering/filter-model.ts","../../../../../../libs/grid/src/lib/core/internal/virtualization.ts","../../../../../../libs/grid/src/lib/core/types.ts","../../../../../../libs/grid/src/lib/core/plugin/base-plugin.ts","../../../../../../libs/grid/src/lib/core/plugin/expander-column.ts","../../../../../../libs/grid/src/lib/plugins/filtering/FilteringPlugin.ts"],"sourcesContent":["/**\n * Filter Model Core Logic\n *\n * Pure functions for filtering operations.\n */\n\nimport type { FilterModel } from './types';\n\n/**\n * Sentinel value used in set-filter unique values to represent rows with\n * no value (null, undefined, empty array via filterValue extractor).\n * Exported so server-side implementations can use the same constant.\n */\nexport const BLANK_FILTER_VALUE = '(Blank)';\n\n/**\n * Convert a value to a comparable number.\n * Handles Date objects, numeric values, and date/ISO strings.\n */\nfunction toNumeric(value: unknown): number {\n if (value instanceof Date) return value.getTime();\n const n = Number(value);\n if (!isNaN(n)) return n;\n // Try parsing as a date string (ISO 8601, etc.)\n const d = new Date(value as string);\n return d.getTime(); // NaN if unparseable\n}\n\n/**\n * Check if a single row matches a filter condition.\n *\n * @param row - The row data object\n * @param filter - The filter to apply\n * @param caseSensitive - Whether text comparisons are case sensitive\n * @param filterValue - Optional extractor for complex cell values (arrays, objects)\n * @returns True if the row matches the filter\n */\nexport function matchesFilter(\n row: Record<string, unknown>,\n filter: FilterModel,\n caseSensitive = false,\n filterValue?: (value: unknown, row: Record<string, unknown>) => unknown | unknown[],\n): boolean {\n const rawValue = row[filter.field];\n\n // Handle blank/notBlank first - these work on null/undefined/empty\n if (filter.operator === 'blank') {\n return rawValue == null || rawValue === '';\n }\n if (filter.operator === 'notBlank') {\n return rawValue != null && rawValue !== '';\n }\n\n // When a filterValue extractor is present, use array-aware matching for set operators.\n // Each extracted value is checked individually against the filter set.\n if (filterValue && (filter.operator === 'notIn' || filter.operator === 'in')) {\n const extracted = filterValue(rawValue, row);\n const values = Array.isArray(extracted) ? extracted : extracted != null ? [extracted] : [];\n\n if (filter.operator === 'notIn') {\n // Row is hidden if ANY extracted value is in the excluded set.\n // Empty values array (null/empty cell) → controlled by BLANK_FILTER_VALUE sentinel.\n const excluded = filter.value;\n if (!Array.isArray(excluded)) return true;\n if (values.length === 0) return !excluded.includes(BLANK_FILTER_VALUE);\n return !values.some((v) => excluded.includes(v));\n }\n if (filter.operator === 'in') {\n // Row passes if ANY extracted value is in the included set.\n // Empty values array (null/empty cell) → controlled by BLANK_FILTER_VALUE sentinel.\n const included = filter.value;\n if (!Array.isArray(included)) return false;\n if (values.length === 0) return included.includes(BLANK_FILTER_VALUE);\n return values.some((v) => included.includes(v));\n }\n }\n\n // Set operators handle null explicitly: null is never \"in\" a set,\n // and null is never excluded by \"notIn\" (it's not a listed value).\n if (filter.operator === 'notIn') {\n if (rawValue == null) return true;\n return Array.isArray(filter.value) && !filter.value.includes(rawValue);\n }\n if (filter.operator === 'in') {\n return Array.isArray(filter.value) && filter.value.includes(rawValue);\n }\n\n // Null/undefined values don't match other filters\n if (rawValue == null) return false;\n\n // Prepare values for comparison\n const stringValue = String(rawValue);\n const compareValue = caseSensitive ? stringValue : stringValue.toLowerCase();\n const compareFilterValue = caseSensitive ? String(filter.value) : String(filter.value).toLowerCase();\n\n switch (filter.operator) {\n // Text operators\n case 'contains':\n return compareValue.includes(compareFilterValue);\n\n case 'notContains':\n return !compareValue.includes(compareFilterValue);\n\n case 'equals':\n return compareValue === compareFilterValue;\n\n case 'notEquals':\n return compareValue !== compareFilterValue;\n\n case 'startsWith':\n return compareValue.startsWith(compareFilterValue);\n\n case 'endsWith':\n return compareValue.endsWith(compareFilterValue);\n\n // Number/Date operators (use toNumeric for Date objects and date strings)\n case 'lessThan':\n return toNumeric(rawValue) < toNumeric(filter.value);\n\n case 'lessThanOrEqual':\n return toNumeric(rawValue) <= toNumeric(filter.value);\n\n case 'greaterThan':\n return toNumeric(rawValue) > toNumeric(filter.value);\n\n case 'greaterThanOrEqual':\n return toNumeric(rawValue) >= toNumeric(filter.value);\n\n case 'between':\n return toNumeric(rawValue) >= toNumeric(filter.value) && toNumeric(rawValue) <= toNumeric(filter.valueTo);\n\n default:\n return true;\n }\n}\n\n/**\n * Filter rows based on multiple filter conditions (AND logic).\n * All filters must match for a row to be included.\n *\n * @param rows - The rows to filter\n * @param filters - Array of filters to apply\n * @param caseSensitive - Whether text comparisons are case sensitive\n * @param filterValues - Optional map of field → value extractor for complex columns\n * @returns Filtered rows\n */\nexport function filterRows<T extends Record<string, unknown>>(\n rows: T[],\n filters: FilterModel[],\n caseSensitive = false,\n filterValues?: Map<string, (value: unknown, row: T) => unknown | unknown[]>,\n): T[] {\n if (!filters.length) return rows;\n return rows.filter((row) =>\n filters.every((f) =>\n matchesFilter(\n row,\n f,\n caseSensitive,\n filterValues?.get(f.field) as\n | ((value: unknown, row: Record<string, unknown>) => unknown | unknown[])\n | undefined,\n ),\n ),\n );\n}\n\n/**\n * Compute a cache key for a set of filters.\n * Used for memoization of filter results.\n *\n * @param filters - Array of filters\n * @returns Stable string key for the filter set\n */\nexport function computeFilterCacheKey(filters: FilterModel[]): string {\n return JSON.stringify(\n filters.map((f) => ({\n field: f.field,\n operator: f.operator,\n value: f.value,\n valueTo: f.valueTo,\n })),\n );\n}\n\n/**\n * Extract unique values from a field across all rows.\n * Useful for populating \"set\" filter dropdowns.\n *\n * When `filterValue` is provided, the extractor is called for each row's cell value.\n * If it returns an array, each element is added individually (flattened).\n * This enables complex-valued cells (e.g., arrays of objects) to expose\n * their individual filterable values.\n *\n * @param rows - The rows to extract values from\n * @param field - The field name\n * @param filterValue - Optional extractor for complex cell values\n * @returns Sorted array of unique non-null values\n */\nexport function getUniqueValues<T extends Record<string, unknown>>(\n rows: T[],\n field: string,\n filterValue?: (value: unknown, row: T) => unknown | unknown[],\n): unknown[] {\n const values = new Set<unknown>();\n let hasBlank = false;\n for (const row of rows) {\n const cellValue = row[field];\n if (filterValue) {\n const extracted = filterValue(cellValue, row);\n if (Array.isArray(extracted)) {\n if (extracted.length === 0) {\n hasBlank = true;\n }\n for (const v of extracted) {\n if (v != null) values.add(v);\n }\n } else if (extracted != null) {\n values.add(extracted);\n } else {\n hasBlank = true;\n }\n } else {\n if (cellValue != null) {\n values.add(cellValue);\n }\n }\n }\n // When a filterValue extractor is present and some rows have no values,\n // include a \"(Blank)\" sentinel so users can explicitly filter empty rows.\n if (filterValue && hasBlank) {\n values.add(BLANK_FILTER_VALUE);\n }\n return [...values].sort((a, b) => {\n // Handle mixed types gracefully\n if (typeof a === 'number' && typeof b === 'number') {\n return a - b;\n }\n return String(a).localeCompare(String(b));\n });\n}\n\n/**\n * Extract unique values for multiple fields in a single pass through the rows.\n * This is more efficient than calling `getUniqueValues` N times when\n * computing derived state for several set filters at once.\n *\n * @param rows - The rows to extract values from\n * @param fields - Array of { field, filterValue? } descriptors\n * @returns Map of field → sorted unique values (same contract as `getUniqueValues`)\n */\nexport function getUniqueValuesBatch<T extends Record<string, unknown>>(\n rows: T[],\n fields: { field: string; filterValue?: (value: unknown, row: T) => unknown | unknown[] }[],\n): Map<string, unknown[]> {\n // Per-field accumulators\n const acc = new Map<string, { values: Set<unknown>; hasBlank: boolean; hasExtractor: boolean }>();\n for (const { field, filterValue } of fields) {\n acc.set(field, { values: new Set(), hasBlank: false, hasExtractor: !!filterValue });\n }\n\n // Single pass through all rows\n for (const row of rows) {\n for (const { field, filterValue } of fields) {\n const entry = acc.get(field)!;\n const cellValue = row[field];\n if (filterValue) {\n const extracted = filterValue(cellValue, row);\n if (Array.isArray(extracted)) {\n if (extracted.length === 0) entry.hasBlank = true;\n for (const v of extracted) {\n if (v != null) entry.values.add(v);\n }\n } else if (extracted != null) {\n entry.values.add(extracted);\n } else {\n entry.hasBlank = true;\n }\n } else {\n if (cellValue != null) entry.values.add(cellValue);\n }\n }\n }\n\n // Build sorted output\n const result = new Map<string, unknown[]>();\n for (const [field, { values, hasBlank, hasExtractor }] of acc) {\n if (hasExtractor && hasBlank) values.add(BLANK_FILTER_VALUE);\n result.set(\n field,\n [...values].sort((a, b) => {\n if (typeof a === 'number' && typeof b === 'number') return a - b;\n return String(a).localeCompare(String(b));\n }),\n );\n }\n return result;\n}\n","/**\n * Row Virtualization Module\n *\n * Provides all virtualization-related functionality for the grid:\n *\n * 1. **Position Cache** (index-based): Maps row index → {offset, height, measured}\n * - Rebuilt when row count changes (expand/collapse, filter)\n * - Used for scroll position → row index lookups (binary search)\n *\n * 2. **Height Cache** (identity-based): Maps row identity → height\n * - Persists across expand/collapse\n * - Uses rowId string keys when available, WeakMap otherwise\n * - Synthetic rows use __rowCacheKey for stable identity\n *\n * 3. **Virtual Window**: Computes which rows to render based on scroll position\n *\n * 4. **Row Measurement**: Measures rendered row heights from DOM\n */\n\n// #region Types\n\n/**\n * Position entry for a single row in the position cache.\n * @category Plugin Development\n */\nexport interface RowPosition {\n /** Cumulative offset from top in pixels */\n offset: number;\n /** Row height in pixels */\n height: number;\n /** Whether this is a measured value (true) or estimate (false) */\n measured: boolean;\n}\n\n/**\n * Height cache that persists row heights across position cache rebuilds.\n * Uses dual storage: Map for string keys (rowId, __rowCacheKey) and WeakMap for object refs.\n * @category Plugin Development\n */\nexport interface HeightCache {\n /** Heights keyed by string (for synthetic rows with __rowCacheKey or rowId-keyed rows) */\n byKey: Map<string, number>;\n /** Heights keyed by object reference (for data rows without rowId) */\n byRef: WeakMap<object, number>;\n}\n\n/**\n * Configuration for the position cache.\n */\nexport interface PositionCacheConfig<T = unknown> {\n /** Function to get row ID (if configured) */\n rowId?: (row: T) => string | number;\n}\n\n// #endregion\n\n// #region Height Cache Operations\n\n/**\n * Create a new empty height cache.\n */\nexport function createHeightCache(): HeightCache {\n return {\n byKey: new Map(),\n byRef: new WeakMap(),\n };\n}\n\n/**\n * Get the cache key for a row.\n * Returns string for synthetic rows (__rowCacheKey) or rowId-keyed rows,\n * or the row object itself for WeakMap lookup.\n */\nexport function getRowCacheKey<T>(row: T, rowId?: (row: T) => string | number): string | T {\n if (!row || typeof row !== 'object') return row;\n\n // 1. Synthetic rows: plugins MUST add __rowCacheKey\n if ('__rowCacheKey' in row) {\n return (row as { __rowCacheKey: string }).__rowCacheKey;\n }\n\n // 2. rowId property directly on the row (common pattern)\n if ('rowId' in row && (row as { rowId: string | number }).rowId != null) {\n return `id:${(row as { rowId: string | number }).rowId}`;\n }\n\n // 3. User-provided rowId function (if configured)\n if (rowId) {\n return `id:${rowId(row)}`;\n }\n\n // 4. Object identity (for WeakMap)\n return row;\n}\n\n/**\n * Get cached height for a row.\n * @returns Cached height or undefined if not cached\n */\nexport function getCachedHeight<T>(\n cache: HeightCache,\n row: T,\n rowId?: (row: T) => string | number,\n): number | undefined {\n const key = getRowCacheKey(row, rowId);\n\n if (typeof key === 'string') {\n return cache.byKey.get(key);\n }\n\n // Object key - use WeakMap\n if (key && typeof key === 'object') {\n return cache.byRef.get(key);\n }\n\n return undefined;\n}\n\n/**\n * Set cached height for a row.\n */\nexport function setCachedHeight<T>(\n cache: HeightCache,\n row: T,\n height: number,\n rowId?: (row: T) => string | number,\n): void {\n const key = getRowCacheKey(row, rowId);\n\n if (typeof key === 'string') {\n cache.byKey.set(key, height);\n } else if (key && typeof key === 'object') {\n cache.byRef.set(key, height);\n }\n}\n\n// #endregion\n\n// #region Position Cache Operations\n\n/**\n * Rebuild position cache preserving known heights from height cache.\n * Called when row count changes (expand/collapse, filter, data change).\n *\n * @param rows - Array of row data\n * @param heightCache - Height cache with persisted measurements\n * @param estimatedHeight - Estimated height for unmeasured rows\n * @param config - Position cache configuration\n * @param getPluginHeight - Optional function to get height from plugins\n * @returns New position cache\n */\nexport function rebuildPositionCache<T>(\n rows: T[],\n heightCache: HeightCache,\n estimatedHeight: number,\n config: PositionCacheConfig<T>,\n getPluginHeight?: (row: T, index: number) => number | undefined,\n): RowPosition[] {\n const cache: RowPosition[] = new Array(rows.length);\n let offset = 0;\n\n for (let i = 0; i < rows.length; i++) {\n const row = rows[i];\n\n // Height resolution order:\n // 1. Plugin's getRowHeight() (for synthetic rows with known heights)\n let height = getPluginHeight?.(row, i);\n let measured = height !== undefined;\n\n // 2. Cached height from previous measurements\n if (height === undefined) {\n height = getCachedHeight(heightCache, row, config.rowId);\n measured = height !== undefined;\n }\n\n // 3. Fall back to estimate\n if (height === undefined) {\n height = estimatedHeight;\n measured = false;\n }\n\n cache[i] = { offset, height, measured };\n offset += height;\n }\n\n return cache;\n}\n\n/**\n * Update a single row's height in the position cache.\n * Recalculates offsets for all subsequent rows.\n *\n * @param cache - Position cache to update\n * @param index - Row index to update\n * @param newHeight - New measured height\n */\nexport function updateRowHeight(cache: RowPosition[], index: number, newHeight: number): void {\n if (index < 0 || index >= cache.length) return;\n\n const entry = cache[index];\n const heightDiff = newHeight - entry.height;\n\n if (heightDiff === 0) return;\n\n // Update this row\n entry.height = newHeight;\n entry.measured = true;\n\n // Recalculate offsets for all subsequent rows\n for (let i = index + 1; i < cache.length; i++) {\n cache[i].offset += heightDiff;\n }\n}\n\n/**\n * Get total content height from position cache.\n *\n * @param cache - Position cache\n * @returns Total height in pixels\n */\nexport function getTotalHeight(cache: RowPosition[]): number {\n if (cache.length === 0) return 0;\n const last = cache[cache.length - 1];\n return last.offset + last.height;\n}\n\n// #endregion\n\n// #region Binary Search\n\n/**\n * Find the row index at a given scroll offset using binary search.\n * Returns the index of the row that contains the given pixel offset.\n *\n * @param cache - Position cache\n * @param targetOffset - Scroll offset in pixels\n * @returns Row index at that offset, or -1 if cache is empty\n */\nexport function getRowIndexAtOffset(cache: RowPosition[], targetOffset: number): number {\n if (cache.length === 0) return -1;\n if (targetOffset <= 0) return 0;\n\n let low = 0;\n let high = cache.length - 1;\n\n while (low <= high) {\n const mid = Math.floor((low + high) / 2);\n const entry = cache[mid];\n const entryEnd = entry.offset + entry.height;\n\n if (targetOffset < entry.offset) {\n high = mid - 1;\n } else if (targetOffset >= entryEnd) {\n low = mid + 1;\n } else {\n // targetOffset is within this row\n return mid;\n }\n }\n\n // Return the closest row (low will be just past the target)\n return Math.max(0, Math.min(low, cache.length - 1));\n}\n\n// #endregion\n\n// #region Statistics\n\n/**\n * Calculate the average measured height.\n * Used for estimating unmeasured rows.\n *\n * @param cache - Position cache\n * @param defaultHeight - Default height to use if no measurements\n * @returns Average measured height\n */\nexport function calculateAverageHeight(cache: RowPosition[], defaultHeight: number): number {\n let totalHeight = 0;\n let measuredCount = 0;\n\n for (const entry of cache) {\n if (entry.measured) {\n totalHeight += entry.height;\n measuredCount++;\n }\n }\n\n return measuredCount > 0 ? totalHeight / measuredCount : defaultHeight;\n}\n\n/**\n * Count how many rows have been measured.\n *\n * @param cache - Position cache\n * @returns Number of measured rows\n */\nexport function countMeasuredRows(cache: RowPosition[]): number {\n let count = 0;\n for (const entry of cache) {\n if (entry.measured) count++;\n }\n return count;\n}\n\n// #endregion\n// #region Row Measurement\n\n/**\n * Result of measuring rendered row heights.\n */\nexport interface RowMeasurementResult {\n /** Whether any heights changed */\n hasChanges: boolean;\n /** Updated measured row count */\n measuredCount: number;\n /** Updated average height */\n averageHeight: number;\n}\n\n/**\n * Context for measuring rendered rows.\n */\nexport interface RowMeasurementContext<T> {\n /** Position cache to update */\n positionCache: RowPosition[];\n /** Height cache for persistence */\n heightCache: HeightCache;\n /** Row data array */\n rows: T[];\n /** Default row height */\n defaultHeight: number;\n /** Start index of rendered window */\n start: number;\n /** End index of rendered window (exclusive) */\n end: number;\n /** Function to get plugin height for a row */\n getPluginHeight?: (row: T, index: number) => number | undefined;\n /** Function to get row ID for height cache keying */\n getRowId?: (row: T) => string | number;\n}\n\n/**\n * Measure rendered row heights from DOM elements and update caches.\n * Returns measurement statistics for updating virtualization state.\n *\n * @param context - Measurement context with all dependencies\n * @param rowElements - NodeList of rendered row elements\n * @returns Measurement result with flags and statistics\n */\nexport function measureRenderedRowHeights<T>(\n context: RowMeasurementContext<T>,\n rowElements: NodeListOf<Element>,\n): RowMeasurementResult {\n const { positionCache, heightCache, rows, start, end, getPluginHeight, getRowId } = context;\n\n let hasChanges = false;\n\n rowElements.forEach((rowEl) => {\n const rowIndexStr = (rowEl as HTMLElement).dataset.rowIndex;\n if (!rowIndexStr) return;\n\n const rowIndex = parseInt(rowIndexStr, 10);\n if (rowIndex < start || rowIndex >= end || rowIndex >= rows.length) return;\n\n const row = rows[rowIndex];\n\n // Check if a plugin provides the height for this row\n const pluginHeight = getPluginHeight?.(row, rowIndex);\n\n if (pluginHeight !== undefined) {\n // Plugin provides height - use it for position cache\n const currentEntry = positionCache[rowIndex];\n if (!currentEntry.measured || Math.abs(currentEntry.height - pluginHeight) > 1) {\n updateRowHeight(positionCache, rowIndex, pluginHeight);\n hasChanges = true;\n }\n return; // Don't measure DOM for plugin-managed rows\n }\n\n // No plugin height - use DOM measurement\n const measuredHeight = (rowEl as HTMLElement).offsetHeight;\n\n if (measuredHeight > 0) {\n const currentEntry = positionCache[rowIndex];\n\n // Only update if height differs significantly (> 1px to avoid oscillation)\n if (!currentEntry.measured || Math.abs(currentEntry.height - measuredHeight) > 1) {\n updateRowHeight(positionCache, rowIndex, measuredHeight);\n setCachedHeight(heightCache, row, measuredHeight, getRowId);\n hasChanges = true;\n }\n }\n });\n\n // Recompute stats\n const measuredCount = hasChanges ? countMeasuredRows(positionCache) : 0;\n const averageHeight = hasChanges ? calculateAverageHeight(positionCache, context.defaultHeight) : 0;\n\n return { hasChanges, measuredCount, averageHeight };\n}\n\n/**\n * Compute measurement statistics for a position cache, excluding plugin-managed rows.\n * Used after rebuilding position cache to get accurate averages for non-plugin rows.\n *\n * @param cache - Position cache\n * @param rows - Row data array\n * @param defaultHeight - Default height if no measurements\n * @param getPluginHeight - Function to check if plugin manages row height\n * @returns Object with measured count and average height\n */\nexport function computeAverageExcludingPluginRows<T>(\n cache: RowPosition[],\n rows: T[],\n defaultHeight: number,\n getPluginHeight?: (row: T, index: number) => number | undefined,\n): { measuredCount: number; averageHeight: number } {\n let measuredCount = 0;\n let totalMeasured = 0;\n\n for (let i = 0; i < cache.length; i++) {\n const entry = cache[i];\n if (entry.measured) {\n // Only include in average if plugin doesn't provide height for this row\n const pluginHeight = getPluginHeight?.(rows[i], i);\n if (pluginHeight === undefined) {\n totalMeasured += entry.height;\n measuredCount++;\n }\n }\n }\n\n return {\n measuredCount,\n averageHeight: measuredCount > 0 ? totalMeasured / measuredCount : defaultHeight,\n };\n}\n\n// #endregion\n\n// #region Fixed-Height Virtual Window\n\n/**\n * Result of computing a virtual window for fixed-height rows.\n * Used by plugins like FilteringPlugin for virtualized dropdowns.\n */\nexport interface VirtualWindow {\n /** First row index to render (inclusive) */\n start: number;\n /** Last row index to render (exclusive) */\n end: number;\n /** Pixel offset to apply to the rows container (translateY) */\n offsetY: number;\n /** Total height of the scrollable content */\n totalHeight: number;\n}\n\n/** Parameters for computing the virtual window */\nexport interface VirtualWindowParams {\n /** Total number of rows */\n totalRows: number;\n /** Height of the viewport in pixels */\n viewportHeight: number;\n /** Current scroll top position */\n scrollTop: number;\n /** Height of each row in pixels */\n rowHeight: number;\n /** Number of extra rows to render above/below viewport */\n overscan: number;\n}\n\n/**\n * Compute the virtual row window based on scroll position and viewport.\n * Simple fixed-height implementation for plugins needing basic virtualization.\n *\n * @param params - Parameters for computing the window\n * @returns VirtualWindow with start/end indices and transforms\n */\nexport function computeVirtualWindow(params: VirtualWindowParams): VirtualWindow {\n const { totalRows, viewportHeight, scrollTop, rowHeight, overscan } = params;\n\n const visibleRows = Math.ceil(viewportHeight / rowHeight);\n\n // Render overscan rows in each direction (total window = visible + 2*overscan)\n let start = Math.floor(scrollTop / rowHeight) - overscan;\n if (start < 0) start = 0;\n\n let end = start + visibleRows + overscan * 2;\n if (end > totalRows) end = totalRows;\n\n // Ensure start is adjusted if we hit the end\n if (end === totalRows && start > 0) {\n start = Math.max(0, end - visibleRows - overscan * 2);\n }\n\n return {\n start,\n end,\n offsetY: start * rowHeight,\n totalHeight: totalRows * rowHeight,\n };\n}\n\n/**\n * Determine if virtualization should be bypassed for small datasets.\n * When there are very few items, the overhead of virtualization isn't worth it.\n *\n * @param totalRows - Total number of items\n * @param threshold - Bypass threshold (skip virtualization if totalRows <= threshold)\n * @returns True if virtualization should be bypassed\n */\nexport function shouldBypassVirtualization(totalRows: number, threshold: number): boolean {\n return totalRows <= threshold;\n}\n\n// #endregion\n","import type { RowPosition } from './internal/virtualization';\nimport type { PluginQuery } from './plugin/base-plugin';\nimport type { AfterCellRenderContext, AfterRowRenderContext, CellMouseEvent } from './plugin/types';\n\n/**\n * Position entry for a single row in the position cache.\n * Part of variable row height virtualization.\n *\n * Re-exported from position-cache.ts for public API consistency.\n *\n * @see VirtualState.positionCache\n * @category Plugin Development\n */\nexport type RowPositionEntry = RowPosition;\n\n// #region DataGridElement Interface\n/**\n * The compiled web component interface for DataGrid.\n *\n * This interface represents the `<tbw-grid>` custom element, combining\n * the public grid API with standard HTMLElement functionality.\n *\n * @example\n * ```typescript\n * // Query existing grid\n * const grid = document.querySelector('tbw-grid') as DataGridElement<Employee>;\n * grid.rows = employees;\n * grid.addEventListener('cell-click', (e) => console.log(e.detail));\n *\n * // Create grid programmatically\n * import { createGrid } from '@toolbox-web/grid';\n * const grid = createGrid<Employee>({\n * columns: [{ field: 'name' }, { field: 'email' }],\n * });\n * document.body.appendChild(grid);\n * ```\n *\n * @see {@link PublicGrid} for the public API methods and properties\n * @see {@link createGrid} for typed grid creation\n * @see {@link queryGrid} for typed grid querying\n */\nexport interface DataGridElement extends PublicGrid, HTMLElement {}\n// #endregion\n\n// #region PublicGrid Interface\n/**\n * Public API interface for DataGrid component.\n *\n * **Property Getters vs Setters:**\n *\n * Property getters return the EFFECTIVE (resolved) value after merging all config sources.\n * This is the \"current situation\" - what consumers and plugins need to know.\n *\n * Property setters accept input values which are merged into the effective config.\n * Multiple sources can contribute (gridConfig, columns prop, light DOM, individual props).\n *\n * For example:\n * - `grid.fitMode` returns the resolved fitMode (e.g., 'stretch' even if you set undefined)\n * - `grid.columns` returns the effective columns after merging\n * - `grid.gridConfig` returns the full effective config\n */\nexport interface PublicGrid<T = any> {\n /**\n * Full config object. Setter merges with other inputs per precedence rules.\n * Getter returns the effective (resolved) config.\n */\n gridConfig?: GridConfig<T>;\n /**\n * Column definitions.\n * Getter returns effective columns (after merging config, light DOM, inference).\n */\n columns?: ColumnConfig<T>[];\n /** Current row data (after plugin processing like grouping, filtering). */\n rows?: T[];\n /** Resolves once the component has finished initial work (layout, inference). */\n ready?: () => Promise<void>;\n /** Force a layout / measurement pass (e.g. after container resize). */\n forceLayout?: () => Promise<void>;\n /** Return effective resolved config (after inference & precedence). */\n getConfig?: () => Promise<Readonly<GridConfig<T>>>;\n /** Toggle expansion state of a group row by its generated key. */\n toggleGroup?: (key: string) => Promise<void>;\n\n // Custom Styles API\n /**\n * Register custom CSS styles to be injected into the grid.\n * Use this to style custom cell renderers, editors, or detail panels.\n * @param id - Unique identifier for the style block (for removal/updates)\n * @param css - CSS string to inject\n */\n registerStyles?: (id: string, css: string) => void;\n /**\n * Remove previously registered custom styles.\n * @param id - The ID used when registering the styles\n */\n unregisterStyles?: (id: string) => void;\n /**\n * Get list of registered custom style IDs.\n */\n getRegisteredStyles?: () => string[];\n\n // Plugin API\n /**\n * Get a plugin instance by its class.\n *\n * @example\n * ```typescript\n * const selection = grid.getPlugin(SelectionPlugin);\n * if (selection) {\n * selection.selectAll();\n * }\n * ```\n */\n getPlugin?<P>(PluginClass: new (...args: any[]) => P): P | undefined;\n /**\n * Get a plugin instance by its name.\n * Prefer `getPlugin(PluginClass)` for type safety.\n */\n getPluginByName?(name: string): GridPlugin | undefined;\n\n // Shell API\n /**\n * Re-render the shell header (title, column groups, toolbar).\n * Call this after dynamically adding/removing tool panels or toolbar buttons.\n */\n refreshShellHeader?(): void;\n /**\n * Register a custom tool panel in the sidebar.\n *\n * @example\n * ```typescript\n * grid.registerToolPanel({\n * id: 'analytics',\n * title: 'Analytics',\n * icon: '📊',\n * render: (container) => {\n * container.innerHTML = '<div>Charts here...</div>';\n * }\n * });\n * ```\n */\n registerToolPanel?(panel: ToolPanelDefinition): void;\n /**\n * Unregister a previously registered tool panel.\n */\n unregisterToolPanel?(panelId: string): void;\n /**\n * Open the tool panel sidebar.\n */\n openToolPanel?(): void;\n /**\n * Close the tool panel sidebar.\n */\n closeToolPanel?(): void;\n /**\n * Toggle the tool panel sidebar open or closed.\n */\n toggleToolPanel?(): void;\n /**\n * Toggle an accordion section expanded or collapsed within the tool panel.\n * @param sectionId - The ID of the section to toggle\n */\n toggleToolPanelSection?(sectionId: string): void;\n\n // State Persistence API\n /**\n * Get the current column state including order, width, visibility, and sort.\n * Use for persisting user preferences to localStorage or a backend.\n *\n * @example\n * ```typescript\n * const state = grid.getColumnState();\n * localStorage.setItem('gridState', JSON.stringify(state));\n * ```\n */\n getColumnState?(): GridColumnState;\n /**\n * Set/restore the column state.\n * Can be set before or after grid initialization.\n *\n * @example\n * ```typescript\n * const saved = localStorage.getItem('gridState');\n * if (saved) {\n * grid.columnState = JSON.parse(saved);\n * }\n * ```\n */\n columnState?: GridColumnState;\n\n // Loading API\n /**\n * Whether the grid is currently in a loading state.\n * When true, displays a loading overlay with spinner.\n *\n * Can also be set via the `loading` HTML attribute.\n *\n * @example\n * ```typescript\n * // Show loading overlay\n * grid.loading = true;\n * const data = await fetchData();\n * grid.rows = data;\n * grid.loading = false;\n * ```\n */\n loading?: boolean;\n\n /**\n * Set loading state for a specific row.\n * Displays a small spinner indicator on the row.\n *\n * Use when persisting row data or performing row-level async operations.\n *\n * @param rowId - The row's unique identifier (from getRowId)\n * @param loading - Whether the row is loading\n *\n * @example\n * ```typescript\n * // Show loading while saving row\n * grid.setRowLoading('emp-123', true);\n * await saveRow(row);\n * grid.setRowLoading('emp-123', false);\n * ```\n */\n setRowLoading?(rowId: string, loading: boolean): void;\n\n /**\n * Set loading state for a specific cell.\n * Displays a small spinner indicator on the cell.\n *\n * Use when performing cell-level async operations (e.g., validation, lookup).\n *\n * @param rowId - The row's unique identifier (from getRowId)\n * @param field - The column field\n * @param loading - Whether the cell is loading\n *\n * @example\n * ```typescript\n * // Show loading while validating cell\n * grid.setCellLoading('emp-123', 'email', true);\n * const isValid = await validateEmail(email);\n * grid.setCellLoading('emp-123', 'email', false);\n * ```\n */\n setCellLoading?(rowId: string, field: string, loading: boolean): void;\n\n /**\n * Check if a row is currently in loading state.\n * @param rowId - The row's unique identifier\n */\n isRowLoading?(rowId: string): boolean;\n\n /**\n * Check if a cell is currently in loading state.\n * @param rowId - The row's unique identifier\n * @param field - The column field\n */\n isCellLoading?(rowId: string, field: string): boolean;\n\n /**\n * Clear all row and cell loading states.\n */\n clearAllLoading?(): void;\n}\n// #endregion\n\n// #region InternalGrid Interface\n/**\n * Internal-only augmented interface for DataGrid component.\n *\n * Member prefixes indicate accessibility:\n * - `_underscore` = protected members - private outside core, accessible to plugins. Marked with @internal.\n * - `__doubleUnderscore` = deeply internal members - private outside core, only for internal functions.\n *\n * @category Plugin Development\n */\nexport interface InternalGrid<T = any> extends PublicGrid<T>, GridConfig<T> {\n // Element methods available because DataGridElement extends HTMLElement\n querySelector<K extends keyof HTMLElementTagNameMap>(selectors: K): HTMLElementTagNameMap[K] | null;\n querySelector<E extends Element = Element>(selectors: string): E | null;\n querySelectorAll<K extends keyof HTMLElementTagNameMap>(selectors: K): NodeListOf<HTMLElementTagNameMap[K]>;\n querySelectorAll<E extends Element = Element>(selectors: string): NodeListOf<E>;\n _rows: T[];\n _columns: ColumnInternal<T>[];\n /** Visible columns only (excludes hidden). Use for rendering. */\n _visibleColumns: ColumnInternal<T>[];\n _headerRowEl: HTMLElement;\n _bodyEl: HTMLElement;\n _rowPool: RowElementInternal[];\n _resizeController: ResizeController;\n _sortState: { field: string; direction: 1 | -1 } | null;\n /** Original unfiltered/unprocessed rows. @internal */\n sourceRows: T[];\n /** Framework adapter instance (set by Grid directives). @internal */\n __frameworkAdapter?: FrameworkAdapter;\n __originalOrder: T[];\n __rowRenderEpoch: number;\n __didInitialAutoSize?: boolean;\n __lightDomColumnsCache?: ColumnInternal[];\n __originalColumnNodes?: HTMLElement[];\n /** Cell display value cache. @internal */\n __cellDisplayCache?: Map<number, string[]>;\n /** Cache epoch for cell display values. @internal */\n __cellCacheEpoch?: number;\n /** Cached header row count for virtualization. @internal */\n __cachedHeaderRowCount?: number;\n /** Cached flag for whether grid has special columns (custom renderers, etc.). @internal */\n __hasSpecialColumns?: boolean;\n /** Cached flag for whether any plugin has renderRow hooks. @internal */\n __hasRenderRowPlugins?: boolean;\n _gridTemplate: string;\n _virtualization: VirtualState;\n _focusRow: number;\n _focusCol: number;\n /** Currently active edit row index. Injected by EditingPlugin. @internal */\n _activeEditRows?: number;\n /** Whether the grid is in 'grid' editing mode (all rows editable). Injected by EditingPlugin. @internal */\n _isGridEditMode?: boolean;\n /** Snapshots of row data before editing. Injected by EditingPlugin. @internal */\n _rowEditSnapshots?: Map<number, T>;\n /** Get all changed rows. Injected by EditingPlugin. */\n changedRows?: T[];\n /** Get IDs of all changed rows. Injected by EditingPlugin. */\n changedRowIds?: string[];\n effectiveConfig?: GridConfig<T>;\n findHeaderRow?: () => HTMLElement;\n refreshVirtualWindow: (full: boolean, skipAfterRender?: boolean) => boolean;\n updateTemplate?: () => void;\n findRenderedRowElement?: (rowIndex: number) => HTMLElement | null;\n /** Get a row by its ID. Implemented in grid.ts */\n getRow?: (id: string) => T | undefined;\n /** Get a row and its current index by ID. Returns undefined if not found. @internal */\n _getRowEntry: (id: string) => { row: T; index: number } | undefined;\n /** Get the unique ID for a row. Implemented in grid.ts */\n getRowId?: (row: T) => string;\n /** Update a row by ID. Implemented in grid.ts */\n updateRow?: (id: string, changes: Partial<T>, source?: UpdateSource) => void;\n /** Animate a single row. Implemented in grid.ts */\n animateRow?: (rowIndex: number, type: RowAnimationType) => void;\n /** Animate multiple rows. Implemented in grid.ts */\n animateRows?: (rowIndices: number[], type: RowAnimationType) => void;\n /** Animate a row by its ID. Implemented in grid.ts */\n animateRowById?: (rowId: string, type: RowAnimationType) => boolean;\n /** Begin bulk edit on a row. Injected by EditingPlugin. */\n beginBulkEdit?: (rowIndex: number) => void;\n /** Commit active row edit. Injected by EditingPlugin. */\n commitActiveRowEdit?: () => void;\n /** Dispatch cell click to plugin system, returns true if handled */\n _dispatchCellClick?: (event: MouseEvent, rowIndex: number, colIndex: number, cellEl: HTMLElement) => boolean;\n /** Dispatch row click to plugin system, returns true if handled */\n _dispatchRowClick?: (event: MouseEvent, rowIndex: number, row: any, rowEl: HTMLElement) => boolean;\n /** Dispatch header click to plugin system, returns true if handled */\n _dispatchHeaderClick?: (event: MouseEvent, col: ColumnConfig, headerEl: HTMLElement) => boolean;\n /** Dispatch keydown to plugin system, returns true if handled */\n _dispatchKeyDown?: (event: KeyboardEvent) => boolean;\n /** Dispatch cell mouse events for drag operations. Returns true if any plugin started a drag. */\n _dispatchCellMouseDown?: (event: CellMouseEvent) => boolean;\n /** Dispatch cell mouse move during drag. */\n _dispatchCellMouseMove?: (event: CellMouseEvent) => void;\n /** Dispatch cell mouse up to end drag. */\n _dispatchCellMouseUp?: (event: CellMouseEvent) => void;\n /** Call afterCellRender hook on all plugins. Called from rows.ts after each cell is rendered. @internal */\n _afterCellRender?: (context: AfterCellRenderContext<T>) => void;\n /** Check if any plugin has registered an afterCellRender hook. Used to skip hook call for performance. @internal */\n _hasAfterCellRenderHook?: () => boolean;\n /** Call afterRowRender hook on all plugins. Called from rows.ts after each row is rendered. @internal */\n _afterRowRender?: (context: AfterRowRenderContext<T>) => void;\n /** Check if any plugin has registered an afterRowRender hook. Used to skip hook call for performance. @internal */\n _hasAfterRowRenderHook?: () => boolean;\n /** Get horizontal scroll boundary offsets from plugins */\n _getHorizontalScrollOffsets?: (\n rowEl?: HTMLElement,\n focusedCell?: HTMLElement,\n ) => { left: number; right: number; skipScroll?: boolean };\n /** Query all plugins with a generic query and collect responses */\n queryPlugins?: <T>(query: PluginQuery) => T[];\n /** Request emission of column-state-change event (debounced) */\n requestStateChange?: () => void;\n}\n// #endregion\n\n// #region Column Types\n/**\n * Built-in primitive column types with automatic formatting and editing support.\n *\n * - `'string'` - Text content, default text input editor\n * - `'number'` - Numeric content, right-aligned, number input editor\n * - `'date'` - Date content, formatted display, date picker editor\n * - `'boolean'` - True/false, rendered as checkbox\n * - `'select'` - Dropdown selection from `options` array\n *\n * @example\n * ```typescript\n * columns: [\n * { field: 'name', type: 'string' },\n * { field: 'age', type: 'number' },\n * { field: 'hireDate', type: 'date' },\n * { field: 'active', type: 'boolean' },\n * { field: 'department', type: 'select', options: [\n * { label: 'Engineering', value: 'eng' },\n * { label: 'Sales', value: 'sales' },\n * ]},\n * ]\n * ```\n *\n * @see {@link ColumnType} for custom type support\n * @see {@link TypeDefault} for type-level defaults\n */\nexport type PrimitiveColumnType = 'number' | 'string' | 'date' | 'boolean' | 'select';\n\n/**\n * Column type - built-in primitives or custom type strings.\n *\n * Use built-in types for automatic formatting, or define custom types\n * (e.g., 'currency', 'country') with type-level defaults via `typeDefaults`.\n *\n * @example\n * ```typescript\n * // Built-in types\n * { field: 'name', type: 'string' }\n * { field: 'salary', type: 'number' }\n *\n * // Custom types with defaults\n * grid.gridConfig = {\n * columns: [\n * { field: 'salary', type: 'currency' },\n * { field: 'birthCountry', type: 'country' },\n * ],\n * typeDefaults: {\n * currency: {\n * format: (v) => `$${Number(v).toFixed(2)}`,\n * },\n * country: {\n * renderer: (ctx) => `🌍 ${ctx.value}`,\n * },\n * },\n * };\n * ```\n *\n * @see {@link PrimitiveColumnType} for built-in types\n * @see {@link TypeDefault} for defining custom type defaults\n */\nexport type ColumnType = PrimitiveColumnType | (string & {});\n// #endregion\n\n// #region TypeDefault Interface\n/**\n * Type-level defaults for formatters and renderers.\n * Applied to all columns of a given type unless overridden at column level.\n *\n * Note: `editor` and `editorParams` are added via module augmentation when\n * EditingPlugin is imported. `filterPanelRenderer` is added by FilteringPlugin.\n *\n * @example\n * ```typescript\n * typeDefaults: {\n * currency: {\n * format: (value) => new Intl.NumberFormat('en-US', {\n * style: 'currency',\n * currency: 'USD',\n * }).format(value as number),\n * },\n * country: {\n * renderer: (ctx) => {\n * const span = document.createElement('span');\n * span.innerHTML = `<img src=\"/flags/${ctx.value}.svg\" /> ${ctx.value}`;\n * return span;\n * }\n * }\n * }\n * ```\n *\n * @see {@link ColumnViewRenderer} for custom renderer function signature\n * @see {@link ColumnType} for type strings that can have defaults\n * @see {@link GridConfig.typeDefaults} for registering type defaults\n */\nexport interface TypeDefault<TRow = unknown> {\n /**\n * Default formatter for all columns of this type.\n *\n * Transforms the raw cell value into a display string. Use when you need\n * consistent formatting across columns without custom DOM (e.g., currency,\n * percentages, dates with specific locale).\n *\n * **Resolution Priority**: Column `format` → Type `format` → Built-in\n *\n * @example\n * ```typescript\n * typeDefaults: {\n * currency: {\n * format: (value) => new Intl.NumberFormat('en-US', {\n * style: 'currency',\n * currency: 'USD',\n * }).format(value as number),\n * },\n * percentage: {\n * format: (value) => `${(value as number * 100).toFixed(1)}%`,\n * }\n * }\n * ```\n */\n format?: (value: unknown, row: TRow) => string;\n\n /**\n * Default renderer for all columns of this type.\n *\n * Creates custom DOM for the cell content. Use when you need more than\n * text formatting (e.g., icons, badges, interactive elements).\n *\n * **Resolution Priority**: Column `renderer` → Type `renderer` → App-level (adapter) → Built-in\n *\n * @example\n * ```typescript\n * typeDefaults: {\n * status: {\n * renderer: (ctx) => {\n * const badge = document.createElement('span');\n * badge.className = `badge badge-${ctx.value}`;\n * badge.textContent = ctx.value as string;\n * return badge;\n * }\n * },\n * country: {\n * renderer: (ctx) => {\n * const span = document.createElement('span');\n * span.innerHTML = `<img src=\"/flags/${ctx.value}.svg\" /> ${ctx.value}`;\n * return span;\n * }\n * }\n * }\n * ```\n */\n renderer?: ColumnViewRenderer<TRow, unknown>;\n}\n// #endregion\n\n// #region BaseColumnConfig Interface\n/**\n * Base contract for a column configuration.\n *\n * Defines the fundamental properties all columns share. Extended by {@link ColumnConfig}\n * with additional features like custom renderers and grouping.\n *\n * @example\n * ```typescript\n * // Basic column with common properties\n * const columns: BaseColumnConfig<Employee>[] = [\n * {\n * field: 'name',\n * header: 'Full Name',\n * sortable: true,\n * resizable: true,\n * },\n * {\n * field: 'salary',\n * type: 'number',\n * width: 120,\n * format: (value) => `$${value.toLocaleString()}`,\n * sortComparator: (a, b) => a - b,\n * },\n * {\n * field: 'department',\n * type: 'select',\n * options: [\n * { label: 'Engineering', value: 'eng' },\n * { label: 'Sales', value: 'sales' },\n * ],\n * },\n * ];\n * ```\n *\n * @see {@link ColumnConfig} for full column configuration with renderers\n * @see {@link ColumnType} for type options\n */\nexport interface BaseColumnConfig<TRow = any, TValue = any> {\n /** Unique field key referencing property in row objects */\n field: keyof TRow & string;\n /** Visible header label; defaults to capitalized field */\n header?: string;\n /**\n * Column data type.\n *\n * Built-in types: `'string'`, `'number'`, `'date'`, `'boolean'`, `'select'`\n *\n * Custom types (e.g., `'currency'`, `'country'`) can have type-level defaults\n * via `gridConfig.typeDefaults` or framework adapter registries.\n *\n * @default Inferred from first row data\n */\n type?: ColumnType;\n /** Column width in pixels; fixed size (no flexibility) */\n width?: string | number;\n /** Minimum column width in pixels (stretch mode only); when set, column uses minmax(minWidth, 1fr) */\n minWidth?: number;\n /** Whether column can be sorted */\n sortable?: boolean;\n /** Whether column can be resized by user */\n resizable?: boolean;\n /** Optional custom comparator for sorting (a,b) -> number */\n sortComparator?: (a: TValue, b: TValue, rowA: TRow, rowB: TRow) => number;\n /** For select type - available options */\n options?: Array<{ label: string; value: unknown }> | (() => Array<{ label: string; value: unknown }>);\n /** For select - allow multi select */\n multi?: boolean;\n /**\n * Formats the raw cell value into a display string.\n *\n * Used both for **cell rendering** and the **built-in filter panel**:\n * - In cells, the formatted value replaces the raw value as text content.\n * - In the filter panel (set filter), checkbox labels show the formatted value\n * instead of the raw value, and search matches against the formatted text.\n *\n * The `row` parameter is available during cell rendering but is `undefined`\n * when called from the filter panel (standalone value formatting). Avoid\n * accessing `row` properties in format functions intended for filter display.\n *\n * @example\n * ```typescript\n * // Currency formatter — works in both cells and filter panel\n * {\n * field: 'price',\n * format: (value) => `$${Number(value).toFixed(2)}`,\n * }\n *\n * // ID-to-name lookup — filter panel shows names, not IDs\n * {\n * field: 'departmentId',\n * format: (value) => departmentMap.get(value as string) ?? String(value),\n * }\n * ```\n */\n format?: (value: TValue, row: TRow) => string;\n /** Arbitrary extra metadata */\n meta?: Record<string, unknown>;\n}\n// #endregion\n\n// #region ColumnConfig Interface\n/**\n * Full column configuration including custom renderers, editors, and grouping metadata.\n *\n * Extends {@link BaseColumnConfig} with additional features for customizing\n * how cells are displayed and edited.\n *\n * @example\n * ```typescript\n * const columns: ColumnConfig<Employee>[] = [\n * // Basic sortable column\n * { field: 'id', header: 'ID', width: 60, sortable: true },\n *\n * // Column with custom renderer\n * {\n * field: 'name',\n * header: 'Employee',\n * renderer: (ctx) => {\n * const div = document.createElement('div');\n * div.innerHTML = `<img src=\"${ctx.row.avatar}\" /><span>${ctx.value}</span>`;\n * return div;\n * },\n * },\n *\n * // Column with custom header\n * {\n * field: 'email',\n * headerLabelRenderer: (ctx) => `${ctx.value} 📧`,\n * },\n *\n * // Editable column (requires EditingPlugin)\n * {\n * field: 'status',\n * editable: true,\n * editor: (ctx) => {\n * const select = document.createElement('select');\n * // ... editor implementation\n * return select;\n * },\n * },\n *\n * // Hidden column (can be shown via VisibilityPlugin)\n * { field: 'internalNotes', hidden: true },\n * ];\n * ```\n *\n * @see {@link BaseColumnConfig} for basic column properties\n * @see {@link ColumnViewRenderer} for custom cell renderers\n * @see {@link ColumnEditorSpec} for custom cell editors\n * @see {@link HeaderRenderer} for custom header renderers\n */\nexport interface ColumnConfig<TRow = any> extends BaseColumnConfig<TRow, any> {\n /**\n * Optional custom cell renderer function. Alias for `viewRenderer`.\n * Can return an HTMLElement, a Node, or an HTML string (which will be sanitized).\n *\n * @example\n * ```typescript\n * // Simple string template\n * renderer: (ctx) => `<span class=\"badge\">${ctx.value}</span>`\n *\n * // DOM element\n * renderer: (ctx) => {\n * const el = document.createElement('span');\n * el.textContent = ctx.value;\n * return el;\n * }\n * ```\n */\n renderer?: ColumnViewRenderer<TRow, any>;\n /** Optional custom view renderer used instead of default text rendering */\n viewRenderer?: ColumnViewRenderer<TRow, any>;\n /** External view spec (lets host app mount any framework component) */\n externalView?: {\n component: unknown;\n props?: Record<string, unknown>;\n mount?: (options: {\n placeholder: HTMLElement;\n context: CellRenderContext<TRow, unknown>;\n spec: unknown;\n }) => void | { dispose?: () => void };\n };\n /** Whether the column is initially hidden */\n hidden?: boolean;\n /** Prevent this column from being hidden programmatically */\n lockVisible?: boolean;\n /**\n * Dynamic CSS class(es) for cells in this column.\n * Called for each cell during rendering. Return class names to add to the cell element.\n *\n * @example\n * ```typescript\n * // Highlight negative values\n * cellClass: (value, row, column) => value < 0 ? ['negative', 'text-red'] : []\n *\n * // Status-based styling\n * cellClass: (value) => [`status-${value}`]\n * ```\n */\n cellClass?: (value: unknown, row: TRow, column: ColumnConfig<TRow>) => string[];\n\n /**\n * Custom header label renderer. Customize the label content while the grid\n * handles sort icons, filter buttons, resize handles, and click interactions.\n *\n * Use this for simple customizations like adding icons, badges, or units.\n *\n * @example\n * ```typescript\n * // Add required field indicator\n * headerLabelRenderer: (ctx) => `${ctx.value} <span class=\"required\">*</span>`\n *\n * // Add unit to header\n * headerLabelRenderer: (ctx) => {\n * const span = document.createElement('span');\n * span.innerHTML = `${ctx.value}<br/><small>(kg)</small>`;\n * return span;\n * }\n * ```\n */\n headerLabelRenderer?: HeaderLabelRenderer<TRow>;\n\n /**\n * Custom header cell renderer. Complete control over the entire header cell.\n * Resize handles are added automatically for resizable columns.\n *\n * The context provides helper functions to include standard elements:\n * - `renderSortIcon()` - Returns sort indicator element (null if not sortable)\n * - `renderFilterButton()` - Returns filter button (null if not filterable)\n *\n * **Precedence**: `headerRenderer` > `headerLabelRenderer` > `header` > `field`\n *\n * @example\n * ```typescript\n * headerRenderer: (ctx) => {\n * const div = document.createElement('div');\n * div.className = 'custom-header';\n * div.innerHTML = `<span>${ctx.value}</span>`;\n * const sortIcon = ctx.renderSortIcon();\n * if (sortIcon) div.appendChild(sortIcon);\n * return div;\n * }\n * ```\n */\n headerRenderer?: HeaderRenderer<TRow>;\n}\n// #endregion\n\n// #region ColumnConfigMap Type\n/**\n * Array of column configurations.\n * Convenience type alias for `ColumnConfig<TRow>[]`.\n *\n * @example\n * ```typescript\n * const columns: ColumnConfigMap<Employee> = [\n * { field: 'name', header: 'Full Name', sortable: true },\n * { field: 'email', header: 'Email Address' },\n * { field: 'department', type: 'select', options: deptOptions },\n * ];\n *\n * grid.columns = columns;\n * ```\n *\n * @see {@link ColumnConfig} for individual column options\n * @see {@link GridConfig.columns} for setting columns on the grid\n */\nexport type ColumnConfigMap<TRow = any> = ColumnConfig<TRow>[];\n// #endregion\n\n// #region Editor Types\n/**\n * Editor specification for inline cell editing.\n * Supports multiple formats for maximum flexibility.\n *\n * **Format Options:**\n * - `string` - Custom element tag name (e.g., 'my-date-picker')\n * - `function` - Factory function returning an editor element\n * - `object` - External component spec for framework integration\n *\n * @example\n * ```typescript\n * // 1. Custom element tag name\n * columns: [\n * { field: 'date', editor: 'my-date-picker' }\n * ]\n *\n * // 2. Factory function (full control)\n * columns: [\n * {\n * field: 'status',\n * editor: (ctx) => {\n * const select = document.createElement('select');\n * select.innerHTML = `\n * <option value=\"active\">Active</option>\n * <option value=\"inactive\">Inactive</option>\n * `;\n * select.value = ctx.value;\n * select.onchange = () => ctx.commit(select.value);\n * select.onkeydown = (e) => {\n * if (e.key === 'Escape') ctx.cancel();\n * };\n * return select;\n * }\n * }\n * ]\n *\n * // 3. External component (React, Angular, Vue)\n * columns: [\n * {\n * field: 'country',\n * editor: {\n * component: CountrySelect,\n * props: { showFlags: true }\n * }\n * }\n * ]\n * ```\n *\n * @see {@link ColumnEditorContext} for the context passed to factory functions\n */\nexport type ColumnEditorSpec<TRow = unknown, TValue = unknown> =\n | string // custom element tag name\n | ((context: ColumnEditorContext<TRow, TValue>) => HTMLElement | string)\n | {\n /** Arbitrary component reference (class, function, token) */\n component: unknown;\n /** Optional static props passed to mount */\n props?: Record<string, unknown>;\n /** Optional custom mount function; if provided we call it directly instead of emitting an event */\n mount?: (options: {\n placeholder: HTMLElement;\n context: ColumnEditorContext<TRow, TValue>;\n spec: unknown;\n }) => void | { dispose?: () => void };\n };\n\n/**\n * Context object provided to editor factories allowing mutation (commit/cancel) of a cell value.\n *\n * The `commit` and `cancel` functions control the editing lifecycle:\n * - Call `commit(newValue)` to save changes and exit edit mode\n * - Call `cancel()` to discard changes and exit edit mode\n *\n * @example\n * ```typescript\n * const myEditor: ColumnEditorSpec = (ctx: ColumnEditorContext) => {\n * const input = document.createElement('input');\n * input.value = ctx.value;\n * input.className = 'my-editor';\n *\n * // Save on Enter, cancel on Escape\n * input.onkeydown = (e) => {\n * if (e.key === 'Enter') {\n * ctx.commit(input.value);\n * } else if (e.key === 'Escape') {\n * ctx.cancel();\n * }\n * };\n *\n * // Access row data for validation\n * if (ctx.row.locked) {\n * input.disabled = true;\n * }\n *\n * return input;\n * };\n * ```\n *\n * @see {@link ColumnEditorSpec} for editor specification options\n */\nexport interface ColumnEditorContext<TRow = any, TValue = any> {\n /** Underlying full row object for the active edit. */\n row: TRow;\n /** Current cell value (mutable only via commit). */\n value: TValue;\n /** Field name being edited. */\n field: keyof TRow & string;\n /** Column configuration reference. */\n column: ColumnConfig<TRow>;\n /**\n * Stable row identifier (from `getRowId`).\n * Empty string if no `getRowId` is configured.\n */\n rowId: string;\n /** Accept the edit; triggers change tracking + rerender. */\n commit: (newValue: TValue) => void;\n /** Abort edit without persisting changes. */\n cancel: () => void;\n /**\n * Update other fields in this row while the editor is open.\n * Changes are committed with source `'cascade'`, triggering\n * `cell-change` events and `onValueChange` pushes to sibling editors.\n *\n * Useful for editors that affect multiple fields (e.g., an address\n * lookup that sets city + zip + state).\n *\n * @example\n * ```typescript\n * // In a cell-commit listener:\n * grid.addEventListener('cell-commit', (e) => {\n * if (e.detail.field === 'quantity') {\n * e.detail.updateRow({ total: e.detail.row.price * e.detail.value });\n * }\n * });\n * ```\n */\n updateRow: (changes: Partial<TRow>) => void;\n /**\n * Register a callback invoked when the cell's underlying value changes\n * while the editor is open (e.g., via `updateRow()` from another cell's commit).\n *\n * Built-in editors auto-update their input values. Custom/framework editors\n * should use this to stay in sync with external mutations.\n *\n * @example\n * ```typescript\n * const editor = (ctx: ColumnEditorContext) => {\n * const input = document.createElement('input');\n * input.value = String(ctx.value);\n * ctx.onValueChange?.((newValue) => {\n * input.value = String(newValue);\n * });\n * return input;\n * };\n * ```\n */\n onValueChange?: (callback: (newValue: TValue) => void) => void;\n}\n// #endregion\n\n// #region Renderer Types\n/**\n * Context passed to custom view renderers (pure display – no commit helpers).\n *\n * Used by `viewRenderer` and `renderer` column properties to create\n * custom cell content. Return a DOM node or HTML string.\n *\n * @example\n * ```typescript\n * // Status badge renderer\n * const statusRenderer: ColumnViewRenderer = (ctx: CellRenderContext) => {\n * const badge = document.createElement('span');\n * badge.className = `badge badge-${ctx.value}`;\n * badge.textContent = ctx.value;\n * return badge;\n * };\n *\n * // Progress bar using row data\n * const progressRenderer: ColumnViewRenderer = (ctx) => {\n * const bar = document.createElement('div');\n * bar.className = 'progress-bar';\n * bar.style.width = `${ctx.value}%`;\n * bar.title = `${ctx.row.taskName}: ${ctx.value}%`;\n * return bar;\n * };\n *\n * // Return HTML string (simpler, less performant)\n * const htmlRenderer: ColumnViewRenderer = (ctx) => {\n * return `<strong>${ctx.value}</strong>`;\n * };\n * ```\n *\n * @see {@link ColumnViewRenderer} for the renderer function signature\n */\nexport interface CellRenderContext<TRow = any, TValue = any> {\n /** Row object for the cell being rendered. */\n row: TRow;\n /** Value at field. */\n value: TValue;\n /** Field key. */\n field: keyof TRow & string;\n /** Column configuration reference. */\n column: ColumnConfig<TRow>;\n /**\n * The cell DOM element being rendered into.\n * Framework adapters can use this to cache per-cell state (e.g., React roots).\n * @internal\n */\n cellEl?: HTMLElement;\n}\n\n/**\n * Custom view renderer function for cell content.\n *\n * Returns one of:\n * - `Node` - DOM element to display in the cell\n * - `string` - HTML string (parsed and inserted)\n * - `void | null` - Use default text rendering\n *\n * @example\n * ```typescript\n * // DOM element (recommended for interactivity)\n * const avatarRenderer: ColumnViewRenderer<Employee> = (ctx) => {\n * const img = document.createElement('img');\n * img.src = ctx.row.avatarUrl;\n * img.alt = ctx.row.name;\n * img.className = 'avatar';\n * return img;\n * };\n *\n * // HTML string (simpler, good for static content)\n * const emailRenderer: ColumnViewRenderer = (ctx) => {\n * return `<a href=\"mailto:${ctx.value}\">${ctx.value}</a>`;\n * };\n *\n * // Conditional rendering\n * const conditionalRenderer: ColumnViewRenderer = (ctx) => {\n * if (!ctx.value) return null; // Use default\n * return `<em>${ctx.value}</em>`;\n * };\n * ```\n *\n * @see {@link CellRenderContext} for available context properties\n */\nexport type ColumnViewRenderer<TRow = unknown, TValue = unknown> = (\n ctx: CellRenderContext<TRow, TValue>,\n) => Node | string | void | null;\n// #endregion\n\n// #region Header Renderer Types\n/**\n * Context passed to `headerLabelRenderer` for customizing header label content.\n * The framework handles sort icons, filter buttons, resize handles, and click interactions.\n *\n * @example\n * ```typescript\n * headerLabelRenderer: (ctx) => {\n * const span = document.createElement('span');\n * span.innerHTML = `${ctx.value} <span class=\"required\">*</span>`;\n * return span;\n * }\n * ```\n */\nexport interface HeaderLabelContext<TRow = unknown> {\n /** Column configuration reference. */\n column: ColumnConfig<TRow>;\n /** The header text (from column.header or column.field). */\n value: string;\n}\n\n/**\n * Context passed to `headerRenderer` for complete control over header cell content.\n * When using this, you control the header content. Resize handles are added automatically\n * for resizable columns.\n *\n * @example\n * ```typescript\n * headerRenderer: (ctx) => {\n * const div = document.createElement('div');\n * div.className = 'custom-header';\n * div.innerHTML = `<span>${ctx.value}</span>`;\n * // Optionally include sort icon\n * const sortIcon = ctx.renderSortIcon();\n * if (sortIcon) div.appendChild(sortIcon);\n * return div;\n * }\n * ```\n */\nexport interface HeaderCellContext<TRow = unknown> {\n /** Column configuration reference. */\n column: ColumnConfig<TRow>;\n /** The header text (from column.header or column.field). */\n value: string;\n /** Current sort state for this column. */\n sortState: 'asc' | 'desc' | null;\n /** Whether the column has an active filter. */\n filterActive: boolean;\n /** The header cell DOM element being rendered into. */\n cellEl: HTMLElement;\n /**\n * Render the standard sort indicator icon.\n * Returns null if column is not sortable.\n */\n renderSortIcon: () => HTMLElement | null;\n /**\n * Render the standard filter button.\n * Returns null if FilteringPlugin is not active or column is not filterable.\n * Note: The actual button is added by FilteringPlugin's afterRender hook.\n */\n renderFilterButton: () => HTMLElement | null;\n}\n\n/**\n * Header label renderer function type.\n * Customize the label while framework handles sort icons, filter buttons, resize handles.\n *\n * Use this for simple label customizations without taking over the entire header.\n * The grid automatically appends sort icons, filter buttons, and resize handles.\n *\n * @example\n * ```typescript\n * // Add required indicator\n * const requiredHeader: HeaderLabelRenderer = (ctx) => {\n * return `${ctx.value} <span style=\"color: red;\">*</span>`;\n * };\n *\n * // Add unit suffix\n * const priceHeader: HeaderLabelRenderer = (ctx) => {\n * const span = document.createElement('span');\n * span.innerHTML = `${ctx.value} <small>(USD)</small>`;\n * return span;\n * };\n *\n * // Column config usage\n * columns: [\n * { field: 'name', headerLabelRenderer: requiredHeader },\n * { field: 'price', headerLabelRenderer: priceHeader },\n * ]\n * ```\n *\n * @see {@link HeaderLabelContext} for context properties\n * @see {@link HeaderRenderer} for full header control\n */\nexport type HeaderLabelRenderer<TRow = unknown> = (ctx: HeaderLabelContext<TRow>) => Node | string | void | null;\n\n/**\n * Header cell renderer function type.\n * Full control over header cell content. User is responsible for all content and interactions.\n *\n * When using this, you have complete control but must manually include\n * sort icons, filter buttons, and resize handles using the helper functions.\n *\n * @example\n * ```typescript\n * // Custom header with all standard elements\n * const customHeader: HeaderRenderer = (ctx) => {\n * const div = document.createElement('div');\n * div.className = 'custom-header';\n * div.innerHTML = `<span class=\"label\">${ctx.value}</span>`;\n *\n * // Add sort icon (returns null if not sortable)\n * const sortIcon = ctx.renderSortIcon();\n * if (sortIcon) div.appendChild(sortIcon);\n *\n * // Add filter button (returns null if not filterable)\n * const filterBtn = ctx.renderFilterButton();\n * if (filterBtn) div.appendChild(filterBtn);\n *\n * // Resize handles are added automatically for resizable columns\n * return div;\n * };\n *\n * // Minimal header (no sort/resize)\n * const minimalHeader: HeaderRenderer = (ctx) => {\n * return `<div class=\"minimal\">${ctx.value}</div>`;\n * };\n *\n * // Column config usage\n * columns: [\n * { field: 'name', headerRenderer: customHeader },\n * ]\n * ```\n *\n * @see {@link HeaderCellContext} for context properties and helper functions\n * @see {@link HeaderLabelRenderer} for simpler label-only customization\n */\nexport type HeaderRenderer<TRow = unknown> = (ctx: HeaderCellContext<TRow>) => Node | string | void | null;\n// #endregion\n\n// #region Framework Adapter Interface\n/**\n * Framework adapter interface for handling framework-specific component instantiation.\n * Allows framework libraries (Angular, React, Vue) to register handlers that convert\n * declarative light DOM elements into functional renderers/editors.\n *\n * @example\n * ```typescript\n * // In @toolbox-web/grid-angular\n * class AngularGridAdapter implements FrameworkAdapter {\n * canHandle(element: HTMLElement): boolean {\n * return element.tagName.startsWith('APP-');\n * }\n * createRenderer(element: HTMLElement): ColumnViewRenderer {\n * return (ctx) => {\n * // Angular-specific instantiation logic\n * const componentRef = createComponent(...);\n * componentRef.setInput('value', ctx.value);\n * return componentRef.location.nativeElement;\n * };\n * }\n * createEditor(element: HTMLElement): ColumnEditorSpec {\n * return (ctx) => {\n * // Angular-specific editor with commit/cancel\n * const componentRef = createComponent(...);\n * componentRef.setInput('value', ctx.value);\n * // Subscribe to commit/cancel outputs\n * return componentRef.location.nativeElement;\n * };\n * }\n * }\n *\n * // User registers adapter once in their app\n * GridElement.registerAdapter(new AngularGridAdapter(injector, appRef));\n * ```\n * @category Framework Adapters\n */\nexport interface FrameworkAdapter {\n /**\n * Determines if this adapter can handle the given element.\n * Typically checks tag name, attributes, or other conventions.\n */\n canHandle(element: HTMLElement): boolean;\n\n /**\n * Creates a view renderer function from a light DOM element.\n * The renderer receives cell context and returns DOM or string.\n */\n createRenderer<TRow = unknown, TValue = unknown>(element: HTMLElement): ColumnViewRenderer<TRow, TValue>;\n\n /**\n * Creates an editor spec from a light DOM element.\n * The editor receives context with commit/cancel and returns DOM.\n * Returns undefined if no editor template is registered, allowing the grid\n * to use its default built-in editors.\n */\n createEditor<TRow = unknown, TValue = unknown>(element: HTMLElement): ColumnEditorSpec<TRow, TValue> | undefined;\n\n /**\n * Creates a tool panel renderer from a light DOM element.\n * The renderer receives a container element and optionally returns a cleanup function.\n */\n createToolPanelRenderer?(element: HTMLElement): ((container: HTMLElement) => void | (() => void)) | undefined;\n\n /**\n * Gets type-level defaults from an application-level registry.\n * Used by Angular's `GridTypeRegistry` and React's `GridTypeProvider`.\n *\n * @param type - The column type (e.g., 'date', 'currency', 'country')\n * @returns Type defaults for renderer/editor, or undefined if not registered\n */\n getTypeDefault?<TRow = unknown>(type: string): TypeDefault<TRow> | undefined;\n\n /**\n * Called when a cell's content is about to be wiped (e.g., when exiting edit mode,\n * scroll-recycling a row, or rebuilding a row).\n *\n * Framework adapters should use this to properly destroy cached views/components\n * associated with the cell to prevent memory leaks.\n *\n * @param cellEl - The cell element whose content is being released\n */\n releaseCell?(cellEl: HTMLElement): void;\n\n /**\n * Unmount a specific framework container and free its resources.\n *\n * Called by the grid core (e.g., MasterDetailPlugin) when a container\n * created by the adapter is about to be removed from the DOM.\n * The adapter should destroy the associated framework instance\n * (React root, Vue app, Angular view) and remove it from tracking arrays.\n *\n * @param container - The container element returned by a create* method\n */\n unmount?(container: HTMLElement): void;\n}\n// #endregion\n\n// #region Internal Types\n\n/**\n * Column internal properties used during light DOM parsing.\n * Stores attribute-based names before they're resolved to actual functions.\n * @internal\n */\nexport interface ColumnParsedAttributes {\n /** Editor name from `editor` attribute (resolved later) */\n __editorName?: string;\n /** Renderer name from `renderer` attribute (resolved later) */\n __rendererName?: string;\n}\n\n/**\n * Extended column config used internally.\n * Includes all internal properties needed during grid lifecycle.\n *\n * Plugin developers may need to access these when working with\n * column caching and compiled templates.\n *\n * @example\n * ```typescript\n * import type { ColumnInternal } from '@toolbox-web/grid';\n *\n * class MyPlugin extends BaseGridPlugin {\n * afterRender(): void {\n * // Access internal column properties\n * const columns = this.columns as ColumnInternal[];\n * for (const col of columns) {\n * // Check if column was auto-sized\n * if (col.__autoSized) {\n * console.log(`${col.field} was auto-sized`);\n * }\n * }\n * }\n * }\n * ```\n *\n * @see {@link ColumnConfig} for public column properties\n * @category Plugin Development\n * @internal\n */\nexport interface ColumnInternal<T = any> extends ColumnConfig<T>, ColumnParsedAttributes {\n __autoSized?: boolean;\n __userResized?: boolean;\n __renderedWidth?: number;\n /** Original configured width (for reset on double-click) */\n __originalWidth?: number;\n __viewTemplate?: HTMLElement;\n __editorTemplate?: HTMLElement;\n __headerTemplate?: HTMLElement;\n __compiledView?: CompiledViewFunction<T>;\n __compiledEditor?: (ctx: EditorExecContext<T>) => string;\n}\n\n/**\n * Row element with internal tracking properties.\n * Used during virtualization and row pooling.\n *\n * @category Plugin Development\n * @internal\n */\nexport interface RowElementInternal extends HTMLElement {\n /** Epoch marker for row render invalidation */\n __epoch?: number;\n /** Reference to the row data object for change detection */\n __rowDataRef?: unknown;\n /** Count of cells currently in edit mode */\n __editingCellCount?: number;\n /** Flag indicating this is a custom-rendered row (group row, etc.) */\n __isCustomRow?: boolean;\n}\n\n/**\n * Type-safe access to element.part API (DOMTokenList-like).\n * Used for CSS ::part styling support.\n * @internal\n */\nexport interface ElementWithPart {\n part?: DOMTokenList;\n}\n\n/**\n * Compiled view function type with optional blocked flag.\n * The __blocked flag is set when a template contains unsafe expressions.\n *\n * @category Plugin Development\n * @internal\n */\nexport interface CompiledViewFunction<T = any> {\n (ctx: CellContext<T>): string;\n /** Set to true when template was blocked due to unsafe expressions */\n __blocked?: boolean;\n}\n\n/**\n * Runtime cell context used internally for compiled template execution.\n *\n * Contains the minimal context needed to render a cell: the row data,\n * cell value, field name, and column configuration.\n *\n * @example\n * ```typescript\n * import type { CellContext, ColumnInternal } from '@toolbox-web/grid';\n *\n * // Used internally by compiled templates\n * const renderCell = (ctx: CellContext) => {\n * return `<span title=\"${ctx.field}\">${ctx.value}</span>`;\n * };\n * ```\n *\n * @see {@link CellRenderContext} for public cell render context\n * @see {@link EditorExecContext} for editor context with commit/cancel\n * @category Plugin Development\n */\nexport interface CellContext<T = any> {\n row: T;\n value: unknown;\n field: string;\n column: ColumnInternal<T>;\n}\n\n/**\n * Internal editor execution context extending the generic cell context with commit helpers.\n *\n * Used internally by the editing system. For public editor APIs,\n * prefer using {@link ColumnEditorContext}.\n *\n * @example\n * ```typescript\n * import type { EditorExecContext } from '@toolbox-web/grid';\n *\n * // Internal editor template execution\n * const execEditor = (ctx: EditorExecContext) => {\n * const input = document.createElement('input');\n * input.value = String(ctx.value);\n * input.onkeydown = (e) => {\n * if (e.key === 'Enter') ctx.commit(input.value);\n * if (e.key === 'Escape') ctx.cancel();\n * };\n * return input;\n * };\n * ```\n *\n * @see {@link ColumnEditorContext} for public editor context\n * @see {@link CellContext} for base cell context\n * @category Plugin Development\n */\nexport interface EditorExecContext<T = any> extends CellContext<T> {\n commit: (newValue: unknown) => void;\n cancel: () => void;\n}\n\n/**\n * Controller managing drag-based column resize lifecycle.\n *\n * Exposed internally for plugins that need to interact with resize behavior.\n *\n * @example\n * ```typescript\n * import type { ResizeController, InternalGrid } from '@toolbox-web/grid';\n *\n * class MyPlugin extends BaseGridPlugin {\n * handleColumnAction(colIndex: number): void {\n * const grid = this.grid as InternalGrid;\n * const resizeCtrl = grid._resizeController;\n *\n * // Check if resize is in progress\n * if (resizeCtrl?.isResizing) {\n * return; // Don't interfere\n * }\n *\n * // Reset column to configured width\n * resizeCtrl?.resetColumn(colIndex);\n * }\n * }\n * ```\n *\n * @see {@link ColumnResizeDetail} for resize event details\n * @category Plugin Development\n */\nexport interface ResizeController {\n start: (e: MouseEvent, colIndex: number, cell: HTMLElement) => void;\n /** Reset a column to its configured width (or auto-size if none configured). */\n resetColumn: (colIndex: number) => void;\n dispose: () => void;\n /** True while a resize drag is in progress (used to suppress header click/sort). */\n isResizing: boolean;\n}\n\n/**\n * Virtual window bookkeeping; modified in-place as scroll position changes.\n *\n * Tracks virtualization state for row rendering. The grid only renders\n * rows within the visible viewport window (start to end) plus overscan.\n *\n * @example\n * ```typescript\n * import type { VirtualState, InternalGrid } from '@toolbox-web/grid';\n *\n * class MyPlugin extends BaseGridPlugin {\n * logVirtualWindow(): void {\n * const grid = this.grid as InternalGrid;\n * const vs = grid.virtualization;\n *\n * console.log(`Row height: ${vs.rowHeight}px`);\n * console.log(`Visible rows: ${vs.start} to ${vs.end}`);\n * console.log(`Virtualization: ${vs.enabled ? 'on' : 'off'}`);\n * }\n * }\n * ```\n *\n * @see {@link GridConfig.rowHeight} for configuring row height\n * @category Plugin Development\n */\nexport interface VirtualState {\n enabled: boolean;\n rowHeight: number;\n /** Threshold for bypassing virtualization (renders all rows if totalRows <= bypassThreshold) */\n bypassThreshold: number;\n start: number;\n end: number;\n /** Faux scrollbar element that provides scroll events (AG Grid pattern) */\n container: HTMLElement | null;\n /** Rows viewport element for measuring visible area height */\n viewportEl: HTMLElement | null;\n /** Spacer element inside faux scrollbar for setting virtual height */\n totalHeightEl: HTMLElement | null;\n\n // --- Variable Row Height Support (Phase 1) ---\n\n /**\n * Position cache for variable row heights.\n * Index-based array mapping row index → {offset, height, measured}.\n * Rebuilt when row count changes (expand/collapse, filter).\n * `null` when using uniform row heights (default).\n */\n positionCache: RowPositionEntry[] | null;\n\n /**\n * Height cache by row identity.\n * Persists row heights across position cache rebuilds.\n * Uses dual storage: Map for string keys (rowId, __rowCacheKey) and WeakMap for object refs.\n */\n heightCache: {\n /** Heights keyed by string (synthetic rows with __rowCacheKey, or rowId-keyed rows) */\n byKey: Map<string, number>;\n /** Heights keyed by object reference (data rows without rowId) */\n byRef: WeakMap<object, number>;\n };\n\n /** Running average of measured row heights. Used for estimating unmeasured rows. */\n averageHeight: number;\n\n /** Number of rows that have been measured. */\n measuredCount: number;\n\n /** Whether variable row height mode is active (rowHeight is a function). */\n variableHeights: boolean;\n\n // --- Cached Geometry (avoid forced layout reads on scroll hot path) ---\n\n /** Cached viewport element height. Updated by ResizeObserver and force-refresh only. */\n cachedViewportHeight: number;\n\n /** Cached faux scrollbar element height. Updated alongside viewport height. */\n cachedFauxHeight: number;\n\n /** Cached scroll-area element height. Updated alongside viewport/faux heights. */\n cachedScrollAreaHeight: number;\n\n /** Cached reference to .tbw-scroll-area element. Set during scroll listener setup. */\n scrollAreaEl: HTMLElement | null;\n}\n\n// RowElementInternal is now defined earlier in the file with all internal properties\n\n/**\n * Union type for input-like elements that have a `value` property.\n * Covers standard form elements and custom elements with value semantics.\n *\n * @category Plugin Development\n * @internal\n */\nexport type InputLikeElement = HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement | { value: unknown };\n// #endregion\n\n// #region Grouping & Footer Public Types\n/**\n * Group row rendering customization options.\n * Controls how group header rows are displayed in the GroupingRowsPlugin.\n *\n * @example\n * ```typescript\n * import { GroupingRowsPlugin } from '@toolbox-web/grid/all';\n *\n * new GroupingRowsPlugin({\n * groupBy: ['department', 'team'],\n * render: {\n * // Group row spans all columns\n * fullWidth: true,\n *\n * // Custom label format\n * formatLabel: (value, depth, key) => {\n * if (depth === 0) return `Department: ${value}`;\n * return `Team: ${value}`;\n * },\n *\n * // Show aggregates in group rows (when not fullWidth)\n * aggregators: {\n * salary: 'sum',\n * age: 'avg',\n * },\n *\n * // Custom CSS class\n * class: 'my-group-row',\n * },\n * });\n * ```\n *\n * @see {@link AggregatorRef} for aggregation options\n */\nexport interface RowGroupRenderConfig {\n /** If true, group rows span all columns (single full-width cell). Default false. */\n fullWidth?: boolean;\n /** Optional label formatter override. Receives raw group value + depth. */\n formatLabel?: (value: unknown, depth: number, key: string) => string;\n /** Optional aggregate overrides per field for group summary cells (only when not fullWidth). */\n aggregators?: Record<string, AggregatorRef>;\n /** Additional CSS class applied to each group row root element. */\n class?: string;\n}\n\n/**\n * Reference to an aggregation function for footer/group summaries.\n *\n * Can be either:\n * - A built-in aggregator name: `'sum'`, `'avg'`, `'min'`, `'max'`, `'count'`\n * - A custom function that calculates the aggregate value\n *\n * @example\n * ```typescript\n * // Built-in aggregator\n * { field: 'amount', aggregator: 'sum' }\n *\n * // Custom aggregator function\n * { field: 'price', aggregator: (rows, field) => {\n * const values = rows.map(r => r[field]).filter(v => v != null);\n * return values.length ? Math.max(...values) : null;\n * }}\n * ```\n *\n * @see {@link RowGroupRenderConfig} for using aggregators in group rows\n */\nexport type AggregatorRef = string | ((rows: unknown[], field: string, column?: unknown) => unknown);\n\n/**\n * Result of automatic column inference from sample rows.\n *\n * When no columns are configured, the grid analyzes the first row of data\n * to automatically generate column definitions with inferred types.\n *\n * @example\n * ```typescript\n * // Automatic inference (no columns configured)\n * grid.rows = [\n * { name: 'Alice', age: 30, active: true, hireDate: new Date() },\n * ];\n * // Grid infers:\n * // - name: type 'string'\n * // - age: type 'number'\n * // - active: type 'boolean'\n * // - hireDate: type 'date'\n *\n * // Access inferred result programmatically\n * const config = await grid.getConfig();\n * console.log(config.columns); // Inferred columns\n * ```\n *\n * @see {@link ColumnConfig} for column configuration options\n * @see {@link ColumnType} for type inference rules\n */\nexport interface InferredColumnResult<TRow = unknown> {\n /** Generated column configurations based on data analysis */\n columns: ColumnConfigMap<TRow>;\n /** Map of field names to their inferred types */\n typeMap: Record<string, ColumnType>;\n}\n\n/**\n * Column sizing mode.\n *\n * - `'fixed'` - Columns use their configured widths. Horizontal scrolling if content overflows.\n * - `'stretch'` - Columns stretch proportionally to fill available width. No horizontal scrolling.\n *\n * @example\n * ```typescript\n * // Fixed widths - good for many columns\n * grid.fitMode = 'fixed';\n *\n * // Stretch to fill - good for few columns\n * grid.fitMode = 'stretch';\n *\n * // Via gridConfig\n * grid.gridConfig = { fitMode: 'stretch' };\n * ```\n */\nexport const FitModeEnum = {\n STRETCH: 'stretch',\n FIXED: 'fixed',\n} as const;\nexport type FitMode = (typeof FitModeEnum)[keyof typeof FitModeEnum]; // evaluates to 'stretch' | 'fixed'\n// #endregion\n\n// #region Plugin Interface\n/**\n * Minimal plugin interface for type-checking.\n * This interface is defined here to avoid circular imports with BaseGridPlugin.\n * All plugins must satisfy this shape (BaseGridPlugin implements it).\n *\n * @example\n * ```typescript\n * // Using plugins in grid config\n * import { SelectionPlugin, FilteringPlugin } from '@toolbox-web/grid/all';\n *\n * grid.gridConfig = {\n * plugins: [\n * new SelectionPlugin({ mode: 'row' }),\n * new FilteringPlugin({ debounceMs: 200 }),\n * ],\n * };\n *\n * // Accessing plugin instance at runtime\n * const selection = grid.getPlugin(SelectionPlugin);\n * if (selection) {\n * selection.selectAll();\n * }\n * ```\n *\n * @category Plugin Development\n */\nexport interface GridPlugin {\n /** Unique plugin identifier */\n readonly name: string;\n /** Plugin version */\n readonly version: string;\n /** CSS styles to inject into the grid */\n readonly styles?: string;\n}\n// #endregion\n\n// #region Grid Config\n/**\n * Grid configuration object - the **single source of truth** for grid behavior.\n *\n * Users can configure the grid via multiple input methods, all of which converge\n * into an effective `GridConfig` internally:\n *\n * **Configuration Input Methods:**\n * - `gridConfig` property - direct assignment of this object\n * - `columns` property - shorthand for `gridConfig.columns`\n * - `fitMode` property - shorthand for `gridConfig.fitMode`\n * - Light DOM `<tbw-grid-column>` - declarative columns (merged into `columns`)\n * - Light DOM `<tbw-grid-header>` - declarative shell header (merged into `shell.header`)\n *\n * **Precedence (when same property set multiple ways):**\n * Individual props (`fitMode`) > `columns` prop > Light DOM > `gridConfig`\n *\n * @example\n * ```ts\n * // Via gridConfig (recommended for complex setups)\n * grid.gridConfig = {\n * columns: [{ field: 'name' }, { field: 'age' }],\n * fitMode: 'stretch',\n * plugins: [new SelectionPlugin()],\n * shell: { header: { title: 'My Grid' } }\n * };\n *\n * // Via individual props (convenience for simple cases)\n * grid.columns = [{ field: 'name' }, { field: 'age' }];\n * grid.fitMode = 'stretch';\n * ```\n */\nexport interface GridConfig<TRow = any> {\n /**\n * Column definitions. Can also be set via `columns` prop or `<tbw-grid-column>` light DOM.\n * @see {@link ColumnConfig} for column options\n * @see {@link ColumnConfigMap}\n */\n columns?: ColumnConfigMap<TRow>;\n /**\n * Dynamic CSS class(es) for data rows.\n * Called for each row during rendering. Return class names to add to the row element.\n *\n * @example\n * ```typescript\n * // Highlight inactive rows\n * rowClass: (row) => row.active ? [] : ['inactive', 'dimmed']\n *\n * // Status-based row styling\n * rowClass: (row) => [`priority-${row.priority}`]\n * ```\n */\n rowClass?: (row: TRow) => string[];\n /** Sizing mode for columns. Can also be set via `fitMode` prop. */\n fitMode?: FitMode;\n\n /**\n * Grid-wide sorting toggle.\n * When false, disables sorting for all columns regardless of their individual `sortable` setting.\n * When true (default), columns with `sortable: true` can be sorted.\n *\n * This affects:\n * - Header click handlers for sorting\n * - Sort indicator visibility\n * - Multi-sort plugin behavior (if loaded)\n *\n * @default true\n *\n * @example\n * ```typescript\n * // Disable all sorting\n * gridConfig = { sortable: false };\n *\n * // Enable sorting (default) - individual columns still need sortable: true\n * gridConfig = { sortable: true };\n * ```\n */\n sortable?: boolean;\n\n /**\n * Grid-wide resizing toggle.\n * When false, disables column resizing for all columns regardless of their individual `resizable` setting.\n * When true (default), columns with `resizable: true` (or resizable not set, since it defaults to true) can be resized.\n *\n * This affects:\n * - Resize handle visibility in header cells\n * - Double-click to auto-size behavior\n *\n * @default true\n *\n * @example\n * ```typescript\n * // Disable all column resizing\n * gridConfig = { resizable: false };\n *\n * // Enable resizing (default) - individual columns can opt out with resizable: false\n * gridConfig = { resizable: true };\n * ```\n */\n resizable?: boolean;\n\n /**\n * Row height in pixels for virtualization calculations.\n * The virtualization system assumes uniform row heights for performance.\n *\n * If not specified, the grid measures the first rendered row's height,\n * which respects the CSS variable `--tbw-row-height` set by themes.\n *\n * Set this explicitly when:\n * - Row content may wrap to multiple lines (also set `--tbw-cell-white-space: normal`)\n * - Using custom row templates with variable content\n * - You want to override theme-defined row height\n * - Rows have different heights based on content (use function form)\n *\n * **Variable Row Heights**: When a function is provided, the grid enables variable height\n * virtualization. Heights are measured on first render and cached by row identity.\n *\n * @default Auto-measured from first row (respects --tbw-row-height CSS variable)\n *\n * @example\n * ```ts\n * // Fixed height for all rows\n * gridConfig = { rowHeight: 56 };\n *\n * // Variable height based on content\n * gridConfig = {\n * rowHeight: (row, index) => row.hasDetails ? 80 : 40,\n * };\n *\n * // Return undefined to trigger DOM auto-measurement\n * gridConfig = {\n * rowHeight: (row) => row.isExpanded ? undefined : 40,\n * };\n * ```\n */\n rowHeight?: number | ((row: TRow, index: number) => number | undefined);\n /**\n * Array of plugin instances.\n * Each plugin is instantiated with its configuration and attached to this grid.\n *\n * @example\n * ```ts\n * plugins: [\n * new SelectionPlugin({ mode: 'range' }),\n * new MultiSortPlugin(),\n * new FilteringPlugin({ debounceMs: 150 }),\n * ]\n * ```\n */\n plugins?: GridPlugin[];\n\n /**\n * Saved column state to restore on initialization.\n * Includes order, width, visibility, sort, and plugin-contributed state.\n */\n columnState?: GridColumnState;\n\n /**\n * Shell configuration for header bar and tool panels.\n * When configured, adds an optional wrapper with title, toolbar, and collapsible side panels.\n */\n shell?: ShellConfig;\n\n /**\n * Grid-wide icon configuration.\n * Provides consistent icons across all plugins (tree, grouping, sorting, etc.).\n * Plugins will use these by default but can override with their own config.\n */\n icons?: GridIcons;\n\n /**\n * Grid-wide animation configuration.\n * Controls animations for expand/collapse, reordering, and other visual transitions.\n * Individual plugins can override these defaults in their own config.\n */\n animation?: AnimationConfig;\n\n /**\n * Custom sort handler for full control over sorting behavior.\n *\n * When provided, this handler is called instead of the built-in sorting logic.\n * Enables custom sorting algorithms, server-side sorting, or plugin-specific sorting.\n *\n * The handler receives:\n * - `rows`: Current row array to sort\n * - `sortState`: Sort field and direction (1 = asc, -1 = desc)\n * - `columns`: Column configurations (for accessing sortComparator)\n *\n * Return the sorted array (sync) or a Promise that resolves to the sorted array (async).\n * For server-side sorting, return a Promise that resolves when data is fetched.\n *\n * @example\n * ```ts\n * // Custom stable sort\n * sortHandler: (rows, state, cols) => {\n * return stableSort(rows, (a, b) => compare(a[state.field], b[state.field]) * state.direction);\n * }\n *\n * // Server-side sorting\n * sortHandler: async (rows, state) => {\n * const response = await fetch(`/api/data?sort=${state.field}&dir=${state.direction}`);\n * return response.json();\n * }\n * ```\n */\n sortHandler?: SortHandler<TRow>;\n\n /**\n * Function to extract a unique identifier from a row.\n * Used by `updateRow()`, `getRow()`, and ID-based tracking.\n *\n * If not provided, falls back to `row.id` or `row._id` if present.\n * Rows without IDs are silently skipped during map building.\n * Only throws when explicitly calling `getRowId()` or `updateRow()` on a row without an ID.\n *\n * @example\n * ```ts\n * // Simple field\n * getRowId: (row) => row.id\n *\n * // Composite key\n * getRowId: (row) => `${row.voyageId}-${row.legNumber}`\n *\n * // UUID field\n * getRowId: (row) => row.uuid\n * ```\n */\n getRowId?: (row: TRow) => string;\n\n /**\n * Type-level renderer and editor defaults.\n *\n * Keys can be:\n * - Built-in types: `'string'`, `'number'`, `'date'`, `'boolean'`, `'select'`\n * - Custom types: `'currency'`, `'country'`, `'status'`, etc.\n *\n * Resolution order (highest priority first):\n * 1. Column-level (`column.renderer` / `column.editor`)\n * 2. Grid-level (`gridConfig.typeDefaults[column.type]`)\n * 3. App-level (Angular `GridTypeRegistry`, React `GridTypeProvider`)\n * 4. Built-in (checkbox for boolean, select for select, etc.)\n * 5. Fallback (plain text / text input)\n *\n * @example\n * ```typescript\n * typeDefaults: {\n * date: { editor: myDatePickerEditor },\n * country: {\n * renderer: (ctx) => {\n * const span = document.createElement('span');\n * span.innerHTML = `<img src=\"/flags/${ctx.value}.svg\" /> ${ctx.value}`;\n * return span;\n * },\n * editor: (ctx) => createCountrySelect(ctx)\n * }\n * }\n * ```\n */\n typeDefaults?: Record<string, TypeDefault<TRow>>;\n\n // #region Accessibility\n\n /**\n * Accessible label for the grid.\n * Sets `aria-label` on the grid's internal table element for screen readers.\n *\n * If not provided and `shell.header.title` is set, the title is used automatically.\n *\n * @example\n * ```ts\n * gridConfig = { gridAriaLabel: 'Employee data' };\n * ```\n */\n gridAriaLabel?: string;\n\n /**\n * ID of an element that describes the grid.\n * Sets `aria-describedby` on the grid's internal table element.\n *\n * @example\n * ```html\n * <p id=\"grid-desc\">This table shows all active employees.</p>\n * <tbw-grid></tbw-grid>\n * ```\n * ```ts\n * gridConfig = { gridAriaDescribedBy: 'grid-desc' };\n * ```\n */\n gridAriaDescribedBy?: string;\n\n // #endregion\n\n // #region Loading\n\n /**\n * Custom renderer for the loading overlay.\n *\n * When provided, replaces the default spinner with custom content.\n * Receives a context object with the current loading size.\n *\n * @example\n * ```typescript\n * // Simple text loading indicator\n * loadingRenderer: () => {\n * const el = document.createElement('div');\n * el.textContent = 'Loading...';\n * return el;\n * }\n *\n * // Custom spinner component\n * loadingRenderer: (ctx) => {\n * const spinner = document.createElement('my-spinner');\n * spinner.size = ctx.size === 'large' ? 48 : 24;\n * return spinner;\n * }\n * ```\n */\n loadingRenderer?: LoadingRenderer;\n\n // #endregion\n}\n// #endregion\n\n// #region Animation\n\n/**\n * Sort state passed to custom sort handlers.\n * Represents the current sorting configuration for a column.\n *\n * @example\n * ```typescript\n * // In a custom sort handler\n * const sortHandler: SortHandler = (rows, sortState, columns) => {\n * const { field, direction } = sortState;\n * console.log(`Sorting by ${field} ${direction === 1 ? 'ASC' : 'DESC'}`);\n *\n * return [...rows].sort((a, b) => {\n * const aVal = a[field];\n * const bVal = b[field];\n * return (aVal < bVal ? -1 : aVal > bVal ? 1 : 0) * direction;\n * });\n * };\n * ```\n *\n * @see {@link SortHandler} for custom sort handler signature\n * @see {@link SortChangeDetail} for sort change events\n */\nexport interface SortState {\n /** Field to sort by */\n field: string;\n /** Sort direction: 1 = ascending, -1 = descending */\n direction: 1 | -1;\n}\n\n/**\n * Custom sort handler function signature.\n *\n * Enables full control over sorting behavior including server-side sorting,\n * custom algorithms, or multi-column sorting.\n *\n * @param rows - Current row array to sort\n * @param sortState - Sort field and direction\n * @param columns - Column configurations (for accessing sortComparator)\n * @returns Sorted array (sync) or Promise resolving to sorted array (async)\n *\n * @example\n * ```typescript\n * // Custom client-side sort with locale awareness\n * const localeSortHandler: SortHandler<Employee> = (rows, state, cols) => {\n * const col = cols.find(c => c.field === state.field);\n * return [...rows].sort((a, b) => {\n * const aVal = String(a[state.field] ?? '');\n * const bVal = String(b[state.field] ?? '');\n * return aVal.localeCompare(bVal) * state.direction;\n * });\n * };\n *\n * // Server-side sorting\n * const serverSortHandler: SortHandler<Employee> = async (rows, state) => {\n * const response = await fetch(\n * `/api/employees?sortBy=${state.field}&dir=${state.direction}`\n * );\n * return response.json();\n * };\n *\n * grid.gridConfig = {\n * sortHandler: localeSortHandler,\n * };\n * ```\n *\n * @see {@link SortState} for the sort state object\n * @see {@link GridConfig.sortHandler} for configuring the handler\n * @see {@link BaseColumnConfig.sortComparator} for column-level comparators\n */\nexport type SortHandler<TRow = any> = (\n rows: TRow[],\n sortState: SortState,\n columns: ColumnConfig<TRow>[],\n) => TRow[] | Promise<TRow[]>;\n\n// #region Loading\n\n/**\n * Loading indicator size variant.\n *\n * - `'large'`: 48x48px max - used for grid-level loading overlay (`grid.loading = true`)\n * - `'small'`: Follows row height - used for row/cell loading states\n *\n * @example\n * ```typescript\n * // Custom loading renderer adapting to size\n * const myLoader: LoadingRenderer = (ctx) => {\n * if (ctx.size === 'large') {\n * // Full overlay spinner\n * return '<div class=\"spinner-lg\"></div>';\n * }\n * // Inline row/cell spinner\n * return '<span class=\"spinner-sm\"></span>';\n * };\n * ```\n *\n * @see {@link LoadingRenderer} for custom loading renderer\n * @see {@link LoadingContext} for context passed to renderers\n */\nexport type LoadingSize = 'large' | 'small';\n\n/**\n * Context passed to custom loading renderers.\n *\n * Provides information about the loading indicator being rendered,\n * allowing the renderer to adapt its appearance based on the size variant.\n *\n * @example\n * ```typescript\n * const myLoadingRenderer: LoadingRenderer = (ctx: LoadingContext) => {\n * if (ctx.size === 'large') {\n * // Full-size spinner for grid-level loading\n * return '<div class=\"large-spinner\"></div>';\n * } else {\n * // Compact spinner for row/cell loading\n * return '<div class=\"small-spinner\"></div>';\n * }\n * };\n * ```\n *\n * @see {@link LoadingRenderer} for the renderer function signature\n * @see {@link LoadingSize} for available size variants\n */\nexport interface LoadingContext {\n /** The size variant being rendered: 'large' for grid-level, 'small' for row/cell */\n size: LoadingSize;\n}\n\n/**\n * Custom loading renderer function.\n * Returns an element or HTML string to display as the loading indicator.\n *\n * Used with the `loadingRenderer` property in {@link GridConfig} to replace\n * the default spinner with custom content.\n *\n * @param context - Context containing size information\n * @returns HTMLElement or HTML string\n *\n * @example\n * ```typescript\n * // Simple text loading indicator\n * const textLoader: LoadingRenderer = () => {\n * const el = document.createElement('div');\n * el.textContent = 'Loading...';\n * return el;\n * };\n *\n * // Custom spinner with size awareness\n * const customSpinner: LoadingRenderer = (ctx) => {\n * const spinner = document.createElement('my-spinner');\n * spinner.size = ctx.size === 'large' ? 48 : 24;\n * return spinner;\n * };\n *\n * // Material Design-style progress bar\n * const progressBar: LoadingRenderer = () => {\n * const container = document.createElement('div');\n * container.className = 'progress-bar-container';\n * container.innerHTML = '<div class=\"progress-bar\"></div>';\n * return container;\n * };\n *\n * grid.gridConfig = {\n * loadingRenderer: customSpinner,\n * };\n * ```\n *\n * @see {@link LoadingContext} for the context object passed to the renderer\n * @see {@link LoadingSize} for size variants ('large' | 'small')\n */\nexport type LoadingRenderer = (context: LoadingContext) => HTMLElement | string;\n\n// #endregion\n\n// #region Data Update Management\n\n/**\n * Indicates the origin of a data change.\n * Used to prevent infinite loops in cascade update handlers.\n *\n * - `'user'`: Direct user interaction via EditingPlugin (typing, selecting)\n * - `'cascade'`: Triggered by `updateRow()` in an event handler\n * - `'api'`: External programmatic update via `grid.updateRow()`\n *\n * @example\n * ```typescript\n * grid.addEventListener('cell-change', (e) => {\n * const { source, field, newValue } = e.detail;\n *\n * // Only cascade updates for user edits\n * if (source === 'user' && field === 'price') {\n * // Update calculated field (marked as 'cascade')\n * grid.updateRow(e.detail.rowId, {\n * total: newValue * e.detail.row.quantity,\n * });\n * }\n *\n * // Ignore cascade updates to prevent infinite loops\n * if (source === 'cascade') return;\n * });\n * ```\n *\n * @see {@link CellChangeDetail} for the event detail containing source\n * @category Data Management\n */\nexport type UpdateSource = 'user' | 'cascade' | 'api';\n\n/**\n * Detail for cell-change event (emitted by core after mutation).\n * This is an informational event that fires for ALL data mutations.\n *\n * Use this event for:\n * - Logging/auditing changes\n * - Cascading updates (updating other fields based on a change)\n * - Syncing changes to external state\n *\n * @example\n * ```typescript\n * grid.addEventListener('cell-change', (e: CustomEvent<CellChangeDetail>) => {\n * const { row, rowId, field, oldValue, newValue, source } = e.detail;\n *\n * console.log(`${field} changed from ${oldValue} to ${newValue}`);\n * console.log(`Change source: ${source}`);\n *\n * // Cascade: update total when price changes\n * if (source === 'user' && field === 'price') {\n * grid.updateRow(rowId, { total: newValue * row.quantity });\n * }\n * });\n * ```\n *\n * @see {@link UpdateSource} for understanding change origins\n * @see {@link CellCommitDetail} for the commit event (editing lifecycle)\n * @category Events\n */\nexport interface CellChangeDetail<TRow = unknown> {\n /** The row object (after mutation) */\n row: TRow;\n /** Stable row identifier */\n rowId: string;\n /** Current index in rows array */\n rowIndex: number;\n /** Field that changed */\n field: string;\n /** Value before change */\n oldValue: unknown;\n /** Value after change */\n newValue: unknown;\n /** All changes passed to updateRow/updateRows (for context) */\n changes: Partial<TRow>;\n /** Origin of this change */\n source: UpdateSource;\n}\n\n/**\n * Batch update specification for updateRows().\n *\n * Used when you need to update multiple rows at once efficiently.\n * The grid will batch all updates and trigger a single re-render.\n *\n * @example\n * ```typescript\n * // Update multiple rows in a single batch\n * const updates: RowUpdate<Employee>[] = [\n * { id: 'emp-1', changes: { status: 'active', updatedAt: new Date() } },\n * { id: 'emp-2', changes: { status: 'inactive' } },\n * { id: 'emp-3', changes: { salary: 75000 } },\n * ];\n *\n * grid.updateRows(updates);\n * ```\n *\n * @see {@link CellChangeDetail} for individual change events\n * @see {@link GridConfig.getRowId} for row identification\n * @category Data Management\n */\nexport interface RowUpdate<TRow = unknown> {\n /** Row identifier (from getRowId) */\n id: string;\n /** Fields to update */\n changes: Partial<TRow>;\n}\n\n// #endregion\n\n/**\n * Animation behavior mode.\n * - `true` or `'on'`: Animations always enabled\n * - `false` or `'off'`: Animations always disabled\n * - `'reduced-motion'`: Respects `prefers-reduced-motion` media query (default)\n *\n * @example\n * ```typescript\n * // Force animations on (ignore system preference)\n * grid.gridConfig = { animation: { mode: 'on' } };\n *\n * // Disable all animations\n * grid.gridConfig = { animation: { mode: false } };\n *\n * // Respect user's accessibility settings (default)\n * grid.gridConfig = { animation: { mode: 'reduced-motion' } };\n * ```\n *\n * @see {@link AnimationConfig} for full animation configuration\n */\nexport type AnimationMode = boolean | 'on' | 'off' | 'reduced-motion';\n\n/**\n * Animation style for visual transitions.\n * - `'slide'`: Slide/transform animation (e.g., expand down, slide left/right)\n * - `'fade'`: Opacity fade animation\n * - `'flip'`: FLIP technique for position changes (First, Last, Invert, Play)\n * - `false`: No animation for this specific feature\n *\n * @example\n * ```typescript\n * // Plugin-specific animation styles\n * new TreePlugin({\n * expandAnimation: 'slide', // Slide children down when expanding\n * });\n *\n * new ReorderPlugin({\n * animation: 'flip', // FLIP animation for column reordering\n * });\n * ```\n *\n * @see {@link AnimationConfig} for grid-wide animation settings\n * @see {@link ExpandCollapseAnimation} for expand/collapse-specific styles\n */\nexport type AnimationStyle = 'slide' | 'fade' | 'flip' | false;\n\n/**\n * Animation style for expand/collapse operations.\n * Subset of AnimationStyle - excludes 'flip' which is for position changes.\n * - `'slide'`: Slide down/up animation for expanding/collapsing content\n * - `'fade'`: Fade in/out animation\n * - `false`: No animation\n *\n * @example\n * ```typescript\n * // Tree rows slide down when expanding\n * new TreePlugin({ expandAnimation: 'slide' });\n *\n * // Row groups fade in/out\n * new GroupingRowsPlugin({ expandAnimation: 'fade' });\n *\n * // Master-detail panels with no animation\n * new MasterDetailPlugin({ expandAnimation: false });\n * ```\n *\n * @see {@link AnimationStyle} for all animation styles\n * @see {@link AnimationConfig} for grid-wide settings\n */\nexport type ExpandCollapseAnimation = 'slide' | 'fade' | false;\n\n/**\n * Type of row animation.\n * - `'change'`: Flash highlight when row data changes (e.g., after cell edit)\n * - `'insert'`: Slide-in animation for newly added rows\n * - `'remove'`: Fade-out animation for rows being removed\n *\n * @example\n * ```typescript\n * // Internal usage - row animation is triggered automatically:\n * // - 'change' after cell-commit event\n * // - 'insert' when rows are added to the grid\n * // - 'remove' when rows are deleted\n *\n * // The animation respects AnimationConfig.mode\n * grid.gridConfig = {\n * animation: { mode: 'on', duration: 300 },\n * };\n * ```\n *\n * @see {@link AnimationConfig} for animation configuration\n */\nexport type RowAnimationType = 'change' | 'insert' | 'remove';\n\n/**\n * Grid-wide animation configuration.\n * Controls global animation behavior - individual plugins define their own animation styles.\n * Duration and easing values set corresponding CSS variables on the grid element.\n *\n * @example\n * ```typescript\n * // Enable animations regardless of system preferences\n * grid.gridConfig = {\n * animation: {\n * mode: 'on',\n * duration: 300,\n * easing: 'cubic-bezier(0.4, 0, 0.2, 1)',\n * },\n * };\n *\n * // Disable all animations\n * grid.gridConfig = {\n * animation: { mode: 'off' },\n * };\n *\n * // Respect user's reduced-motion preference (default)\n * grid.gridConfig = {\n * animation: { mode: 'reduced-motion' },\n * };\n * ```\n *\n * @see {@link AnimationMode} for mode options\n */\nexport interface AnimationConfig {\n /**\n * Global animation mode.\n * @default 'reduced-motion'\n */\n mode?: AnimationMode;\n\n /**\n * Default animation duration in milliseconds.\n * Sets `--tbw-animation-duration` CSS variable.\n * @default 200\n */\n duration?: number;\n\n /**\n * Default easing function.\n * Sets `--tbw-animation-easing` CSS variable.\n * @default 'ease-out'\n */\n easing?: string;\n}\n\n/** Default animation configuration */\nexport const DEFAULT_ANIMATION_CONFIG: Required<Omit<AnimationConfig, 'sort'>> = {\n mode: 'reduced-motion',\n duration: 200,\n easing: 'ease-out',\n};\n\n// #endregion\n\n// #region Grid Icons\n\n/** Icon value - can be a string (text/HTML) or HTMLElement */\nexport type IconValue = string | HTMLElement;\n\n/**\n * Grid-wide icon configuration.\n * All icons are optional - sensible defaults are used when not specified.\n *\n * Icons can be text (including emoji), HTML strings (for SVG), or HTMLElement instances.\n *\n * @example\n * ```typescript\n * grid.gridConfig = {\n * icons: {\n * // Emoji icons\n * expand: '➕',\n * collapse: '➖',\n *\n * // Custom SVG icon\n * sortAsc: '<svg viewBox=\"0 0 16 16\"><path d=\"M8 4l4 8H4z\"/></svg>',\n *\n * // Font icon class (wrap in span)\n * filter: '<span class=\"icon icon-filter\"></span>',\n * },\n * };\n * ```\n *\n * @see {@link IconValue} for allowed icon formats\n */\nexport interface GridIcons {\n /** Expand icon for collapsed items (trees, groups, details). Default: '▶' */\n expand?: IconValue;\n /** Collapse icon for expanded items (trees, groups, details). Default: '▼' */\n collapse?: IconValue;\n /** Sort ascending indicator. Default: '▲' */\n sortAsc?: IconValue;\n /** Sort descending indicator. Default: '▼' */\n sortDesc?: IconValue;\n /** Sort neutral/unsorted indicator. Default: '⇅' */\n sortNone?: IconValue;\n /** Submenu arrow for context menus. Default: '▶' */\n submenuArrow?: IconValue;\n /** Drag handle icon for reordering. Default: '⋮⋮' */\n dragHandle?: IconValue;\n /** Tool panel toggle icon in toolbar. Default: '☰' */\n toolPanel?: IconValue;\n /** Filter icon in column headers. Default: SVG funnel icon */\n filter?: IconValue;\n /** Filter icon when filter is active. Default: same as filter with accent color */\n filterActive?: IconValue;\n /** Print icon for print button. Default: '🖨️' */\n print?: IconValue;\n}\n\n/** Default filter icon SVG */\nconst DEFAULT_FILTER_ICON =\n '<svg viewBox=\"0 0 16 16\" width=\"12\" height=\"12\"><path fill=\"currentColor\" d=\"M6 10.5a.5.5 0 0 1 .5-.5h3a.5.5 0 0 1 0 1h-3a.5.5 0 0 1-.5-.5zm-2-3a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7a.5.5 0 0 1-.5-.5zm-2-3a.5.5 0 0 1 .5-.5h11a.5.5 0 0 1 0 1h-11a.5.5 0 0 1-.5-.5z\"/></svg>';\n\n/** Default icons used when not overridden */\nexport const DEFAULT_GRID_ICONS: Required<GridIcons> = {\n expand: '▶',\n collapse: '▼',\n sortAsc: '▲',\n sortDesc: '▼',\n sortNone: '⇅',\n submenuArrow: '▶',\n dragHandle: '⋮⋮',\n toolPanel: '☰',\n filter: DEFAULT_FILTER_ICON,\n filterActive: DEFAULT_FILTER_ICON,\n print: '🖨️',\n};\n// #endregion\n\n// #region Shell Configuration\n\n/**\n * Shell configuration for the grid's optional header bar and tool panels.\n *\n * The shell provides a wrapper around the grid with:\n * - Header bar with title, toolbar buttons, and custom content\n * - Collapsible side panel for filters, column visibility, settings, etc.\n *\n * @example\n * ```typescript\n * grid.gridConfig = {\n * shell: {\n * header: {\n * title: 'Employee Directory',\n * },\n * toolPanel: {\n * position: 'right',\n * defaultOpen: 'columns', // Open by default\n * },\n * },\n * plugins: [new VisibilityPlugin()], // Adds \"Columns\" panel\n * };\n *\n * // Register custom tool panels\n * grid.registerToolPanel({\n * id: 'filters',\n * title: 'Filters',\n * icon: '🔍',\n * render: (container) => {\n * container.innerHTML = '<div>Filter controls...</div>';\n * },\n * });\n * ```\n *\n * @see {@link ShellHeaderConfig} for header options\n * @see {@link ToolPanelConfig} for tool panel options\n */\nexport interface ShellConfig {\n /** Shell header bar configuration */\n header?: ShellHeaderConfig;\n /** Tool panel configuration */\n toolPanel?: ToolPanelConfig;\n /**\n * Registered tool panels (from plugins, API, or Light DOM).\n * These are the actual panel definitions that can be opened.\n * @internal Set by ConfigManager during merge\n */\n toolPanels?: ToolPanelDefinition[];\n /**\n * Registered header content sections (from plugins or API).\n * Content rendered in the center of the shell header.\n * @internal Set by ConfigManager during merge\n */\n headerContents?: HeaderContentDefinition[];\n}\n\n/**\n * Shell header bar configuration\n */\nexport interface ShellHeaderConfig {\n /** Grid title displayed on the left (optional) */\n title?: string;\n /** Custom toolbar content (rendered before tool panel toggle) */\n toolbarContents?: ToolbarContentDefinition[];\n /**\n * Light DOM header content elements (parsed from <tbw-grid-header> children).\n * @internal Set by ConfigManager during merge\n */\n lightDomContent?: HTMLElement[];\n /**\n * Whether a tool buttons container was found in light DOM.\n * @internal Set by ConfigManager during merge\n */\n hasToolButtonsContainer?: boolean;\n}\n\n/**\n * Tool panel configuration\n */\nexport interface ToolPanelConfig {\n /** Panel position: 'left' | 'right' (default: 'right') */\n position?: 'left' | 'right';\n /** Default panel width in pixels (default: 280) */\n width?: number;\n /** Panel ID to open by default on load */\n defaultOpen?: string;\n /** Whether to persist open/closed state (requires Column State Events) */\n persistState?: boolean;\n /**\n * Close the tool panel when clicking outside of it.\n * When `true`, clicking anywhere outside the tool panel (but inside the grid)\n * will close the panel automatically.\n * @default false\n */\n closeOnClickOutside?: boolean;\n}\n\n/**\n * Toolbar content definition for the shell header toolbar area.\n * Register via `registerToolbarContent()` or use light DOM `<tbw-grid-tool-buttons>`.\n *\n * @example\n * ```typescript\n * grid.registerToolbarContent({\n * id: 'my-toolbar',\n * order: 10,\n * render: (container) => {\n * const btn = document.createElement('button');\n * btn.textContent = 'Refresh';\n * btn.onclick = () => console.log('clicked');\n * container.appendChild(btn);\n * return () => btn.remove();\n * },\n * });\n * ```\n */\nexport interface ToolbarContentDefinition {\n /** Unique content ID */\n id: string;\n /** Content factory - called once when shell header renders */\n render: (container: HTMLElement) => void | (() => void);\n /** Called when content is removed (for cleanup) */\n onDestroy?: () => void;\n /** Order priority (lower = first, default: 100) */\n order?: number;\n}\n\n/**\n * Tool panel definition registered by plugins or consumers.\n *\n * Register via `grid.registerToolPanel()` to add panels to the sidebar.\n * Panels appear as collapsible sections with icons and titles.\n *\n * @example\n * ```typescript\n * grid.registerToolPanel({\n * id: 'filters',\n * title: 'Filters',\n * icon: '🔍',\n * tooltip: 'Filter grid data',\n * order: 10, // Lower = appears first\n * render: (container) => {\n * container.innerHTML = `\n * <div class=\"filter-panel\">\n * <input type=\"text\" placeholder=\"Search...\" />\n * </div>\n * `;\n * // Return cleanup function\n * return () => container.innerHTML = '';\n * },\n * onClose: () => {\n * console.log('Filter panel closed');\n * },\n * });\n * ```\n *\n * @see {@link ShellConfig} for shell configuration\n */\nexport interface ToolPanelDefinition {\n /** Unique panel ID */\n id: string;\n /** Panel title shown in accordion header */\n title: string;\n /** Icon for accordion section header (optional, emoji or SVG) */\n icon?: string;\n /** Tooltip for accordion section header */\n tooltip?: string;\n /** Panel content factory - called when panel section opens */\n render: (container: HTMLElement) => void | (() => void);\n /** Called when panel closes (for cleanup) */\n onClose?: () => void;\n /** Panel order priority (lower = first, default: 100) */\n order?: number;\n}\n\n/**\n * Header content definition for plugins contributing to shell header center section.\n *\n * Register via `grid.registerHeaderContent()` to add content between\n * the title and toolbar buttons.\n *\n * @example\n * ```typescript\n * grid.registerHeaderContent({\n * id: 'row-count',\n * order: 10,\n * render: (container) => {\n * const span = document.createElement('span');\n * span.className = 'row-count';\n * span.textContent = `${grid.rows.length} rows`;\n * container.appendChild(span);\n *\n * // Update on data changes\n * const update = () => span.textContent = `${grid.rows.length} rows`;\n * grid.addEventListener('data-change', update);\n *\n * return () => {\n * grid.removeEventListener('data-change', update);\n * };\n * },\n * });\n * ```\n *\n * @see {@link ShellConfig} for shell configuration\n */\nexport interface HeaderContentDefinition {\n /** Unique content ID */\n id: string;\n /** Content factory - called once when shell header renders */\n render: (container: HTMLElement) => void | (() => void);\n /** Called when content is removed (for cleanup) */\n onDestroy?: () => void;\n /** Order priority (lower = first, default: 100) */\n order?: number;\n}\n// #endregion\n\n// #region Column State (Persistence)\n\n/**\n * State for a single column. Captures user-driven changes at runtime.\n * Plugins can extend this interface via module augmentation to add their own state.\n *\n * Used with `grid.getColumnState()` and `grid.columnState` for persisting\n * user customizations (column widths, order, visibility, sort).\n *\n * @example\n * ```typescript\n * // Save column state to localStorage\n * const state = grid.getColumnState();\n * localStorage.setItem('gridState', JSON.stringify(state));\n *\n * // Restore on page load\n * const saved = localStorage.getItem('gridState');\n * if (saved) {\n * grid.columnState = JSON.parse(saved);\n * }\n *\n * // Example column state structure\n * const state: GridColumnState = {\n * columns: [\n * { field: 'name', order: 0, width: 200, hidden: false },\n * { field: 'email', order: 1, width: 300, hidden: false },\n * { field: 'phone', order: 2, hidden: true }, // Hidden column\n * ],\n * sort: { field: 'name', direction: 1 },\n * };\n * ```\n *\n * @example\n * ```typescript\n * // Plugin augmentation example (in filtering plugin)\n * declare module '@toolbox-web/grid' {\n * interface ColumnState {\n * filter?: FilterValue;\n * }\n * }\n * ```\n *\n * @see {@link GridColumnState} for the full state object\n */\nexport interface ColumnState {\n /** Column field identifier */\n field: string;\n /** Position index after reordering (0-based) */\n order: number;\n /** Width in pixels (undefined = use default) */\n width?: number;\n /** Visibility state */\n visible: boolean;\n /** Sort state (undefined = not sorted). */\n sort?: ColumnSortState;\n}\n\n/**\n * Sort state for a column.\n * Used within {@link ColumnState} to track sort direction and priority.\n *\n * @see {@link ColumnState} for column state persistence\n * @see {@link SortChangeDetail} for sort change events\n */\nexport interface ColumnSortState {\n /** Sort direction */\n direction: 'asc' | 'desc';\n /** Priority for multi-sort (0 = primary, 1 = secondary, etc.) */\n priority: number;\n}\n\n/**\n * Complete grid column state for persistence.\n * Contains state for all columns, including plugin-contributed properties.\n *\n * @example\n * ```typescript\n * // Save state\n * const state = grid.getColumnState();\n * localStorage.setItem('grid-state', JSON.stringify(state));\n *\n * // Restore state\n * grid.columnState = JSON.parse(localStorage.getItem('grid-state'));\n * ```\n *\n * @see {@link ColumnState} for individual column state\n * @see {@link PublicGrid.getColumnState} for retrieving state\n */\nexport interface GridColumnState {\n /** Array of column states. */\n columns: ColumnState[];\n}\n// #endregion\n\n// #region Public Event Detail Interfaces\n/**\n * Detail for a cell click event.\n * Provides full context about the clicked cell including row data.\n *\n * @example\n * ```typescript\n * grid.addEventListener('cell-click', (e: CustomEvent<CellClickDetail>) => {\n * const { row, field, value, rowIndex, colIndex } = e.detail;\n * console.log(`Clicked ${field} = ${value} in row ${rowIndex}`);\\n *\n * // Access the full row data\n * if (row.status === 'pending') {\n * showApprovalDialog(row);\n * }\n * });\n * ```\n *\n * @category Events\n */\nexport interface CellClickDetail<TRow = unknown> {\n /** Zero-based row index of the clicked cell. */\n rowIndex: number;\n /** Zero-based column index of the clicked cell. */\n colIndex: number;\n /** Column configuration object for the clicked cell. */\n column: ColumnConfig<TRow>;\n /** Field name of the clicked column. */\n field: string;\n /** Cell value at the clicked position. */\n value: unknown;\n /** Full row data object. */\n row: TRow;\n /** The clicked cell element. */\n cellEl: HTMLElement;\n /** The original mouse event. */\n originalEvent: MouseEvent;\n}\n\n/**\n * Detail for a row click event.\n * Provides context about the clicked row.\n *\n * @example\n * ```typescript\n * grid.addEventListener('row-click', (e: CustomEvent<RowClickDetail>) => {\n * const { row, rowIndex, rowEl } = e.detail;\n * console.log(`Clicked row ${rowIndex}: ${row.name}`);\n *\n * // Highlight the row\n * rowEl.classList.add('selected');\n *\n * // Open detail panel\n * showDetailPanel(row);\n * });\n * ```\n *\n * @category Events\n */\nexport interface RowClickDetail<TRow = unknown> {\n /** Zero-based row index of the clicked row. */\n rowIndex: number;\n /** Full row data object. */\n row: TRow;\n /** The clicked row element. */\n rowEl: HTMLElement;\n /** The original mouse event. */\n originalEvent: MouseEvent;\n}\n\n/**\n * Detail for a sort change (direction 0 indicates cleared sort).\n *\n * @example\n * ```typescript\n * grid.addEventListener('sort-change', (e: CustomEvent<SortChangeDetail>) => {\n * const { field, direction } = e.detail;\n *\n * if (direction === 0) {\n * console.log(`Sort cleared on ${field}`);\n * } else {\n * const dir = direction === 1 ? 'ascending' : 'descending';\n * console.log(`Sorted by ${field} ${dir}`);\n * }\n *\n * // Fetch sorted data from server\n * fetchData({ sortBy: field, sortDir: direction });\n * });\n * ```\n *\n * @see {@link SortState} for the sort state object\n * @see {@link SortHandler} for custom sort handlers\n * @category Events\n */\nexport interface SortChangeDetail {\n /** Sorted field key. */\n field: string;\n /** Direction: 1 ascending, -1 descending, 0 cleared. */\n direction: 1 | -1 | 0;\n}\n\n/**\n * Column resize event detail containing final pixel width.\n *\n * @example\n * ```typescript\n * grid.addEventListener('column-resize', (e: CustomEvent<ColumnResizeDetail>) => {\n * const { field, width } = e.detail;\n * console.log(`Column ${field} resized to ${width}px`);\n *\n * // Persist to user preferences\n * saveColumnWidth(field, width);\n * });\n * ```\n *\n * @see {@link ColumnState} for persisting column state\n * @see {@link ResizeController} for resize implementation\n * @category Events\n */\nexport interface ColumnResizeDetail {\n /** Resized column field key. */\n field: string;\n /** New width in pixels. */\n width: number;\n}\n\n/**\n * Trigger type for cell activation.\n * - `'keyboard'`: Enter key pressed on focused cell\n * - `'pointer'`: Mouse/touch/pen click on cell\n *\n * @see {@link CellActivateDetail} for the activation event detail\n * @category Events\n */\nexport type CellActivateTrigger = 'keyboard' | 'pointer';\n\n/**\n * Fired when a cell is activated by user interaction (Enter key or click).\n * Unified event for both keyboard and pointer activation.\n *\n * @example\n * ```typescript\n * grid.addEventListener('cell-activate', (e: CustomEvent<CellActivateDetail>) => {\n * const { row, field, value, trigger, cellEl } = e.detail;\n *\n * if (trigger === 'keyboard') {\n * console.log('Activated via Enter key');\n * } else {\n * console.log('Activated via click/tap');\n * }\n *\n * // Start custom editing for specific columns\n * if (field === 'notes') {\n * openNotesEditor(row, cellEl);\n * e.preventDefault(); // Prevent default editing\n * }\n * });\n * ```\n *\n * @see {@link CellClickDetail} for click-only events\n * @see {@link CellActivateTrigger} for trigger types\n * @category Events\n */\nexport interface CellActivateDetail<TRow = unknown> {\n /** Zero-based row index of the activated cell. */\n rowIndex: number;\n /** Zero-based column index of the activated cell. */\n colIndex: number;\n /** Field name of the activated column. */\n field: string;\n /** Cell value at the activated position. */\n value: unknown;\n /** Full row data object. */\n row: TRow;\n /** The activated cell element. */\n cellEl: HTMLElement;\n /** What triggered the activation. */\n trigger: CellActivateTrigger;\n /** The original event (KeyboardEvent for keyboard, MouseEvent/PointerEvent for pointer). */\n originalEvent: KeyboardEvent | MouseEvent | PointerEvent;\n}\n\n/**\n * @deprecated Use `CellActivateDetail` instead. Will be removed in next major version.\n * Kept for backwards compatibility.\n *\n * @category Events\n */\nexport interface ActivateCellDetail {\n /** Zero-based row index now focused. */\n row: number;\n /** Zero-based column index now focused. */\n col: number;\n}\n\n/**\n * Event detail for mounting external view renderers.\n *\n * Emitted when a cell uses an external component spec (React, Angular, Vue)\n * and needs the framework adapter to mount the component.\n *\n * @example\n * ```typescript\n * // Framework adapter listens for this event\n * grid.addEventListener('mount-external-view', (e: CustomEvent<ExternalMountViewDetail>) => {\n * const { placeholder, spec, context } = e.detail;\n * // Mount framework component into placeholder\n * mountComponent(spec.component, placeholder, context);\n * });\n * ```\n *\n * @see {@link ColumnConfig.externalView} for external view spec\n * @see {@link FrameworkAdapter} for adapter interface\n * @category Framework Adapters\n */\nexport interface ExternalMountViewDetail<TRow = unknown> {\n placeholder: HTMLElement;\n spec: unknown;\n context: { row: TRow; value: unknown; field: string; column: unknown };\n}\n\n/**\n * Event detail for mounting external editor renderers.\n *\n * Emitted when a cell uses an external editor component spec and needs\n * the framework adapter to mount the editor with commit/cancel bindings.\n *\n * @example\n * ```typescript\n * // Framework adapter listens for this event\n * grid.addEventListener('mount-external-editor', (e: CustomEvent<ExternalMountEditorDetail>) => {\n * const { placeholder, spec, context } = e.detail;\n * // Mount framework editor with commit/cancel wired\n * mountEditor(spec.component, placeholder, {\n * value: context.value,\n * onCommit: context.commit,\n * onCancel: context.cancel,\n * });\n * });\n * ```\n *\n * @see {@link ColumnEditorSpec} for external editor spec\n * @see {@link FrameworkAdapter} for adapter interface\n * @category Framework Adapters\n */\nexport interface ExternalMountEditorDetail<TRow = unknown> {\n placeholder: HTMLElement;\n spec: unknown;\n context: {\n row: TRow;\n value: unknown;\n field: string;\n column: unknown;\n commit: (v: unknown) => void;\n cancel: () => void;\n };\n}\n\n/**\n * Maps event names to their detail payload types.\n *\n * Use this interface for strongly typed event handling.\n *\n * @example\n * ```typescript\n * // Type-safe event listener\n * function handleEvent<K extends keyof DataGridEventMap>(\n * grid: DataGridElement,\n * event: K,\n * handler: (detail: DataGridEventMap[K]) => void,\n * ): void {\n * grid.addEventListener(event, (e: CustomEvent) => handler(e.detail));\n * }\n *\n * handleEvent(grid, 'cell-click', (detail) => {\n * console.log(detail.field); // Type-safe access\n * });\n * ```\n *\n * @see {@link DataGridCustomEvent} for typed CustomEvent wrapper\n * @see {@link DGEvents} for event name constants\n * @category Events\n */\nexport interface DataGridEventMap<TRow = unknown> {\n 'cell-click': CellClickDetail<TRow>;\n 'row-click': RowClickDetail<TRow>;\n 'cell-activate': CellActivateDetail<TRow>;\n 'cell-change': CellChangeDetail<TRow>;\n 'mount-external-view': ExternalMountViewDetail<TRow>;\n 'mount-external-editor': ExternalMountEditorDetail<TRow>;\n 'sort-change': SortChangeDetail;\n 'column-resize': ColumnResizeDetail;\n /** @deprecated Use 'cell-activate' instead */\n 'activate-cell': ActivateCellDetail;\n 'column-state-change': GridColumnState;\n // Note: 'cell-commit', 'row-commit', 'changed-rows-reset' are added via\n // module augmentation by EditingPlugin when imported\n}\n\n/**\n * Extracts the event detail type for a given event name.\n *\n * Utility type for getting the detail payload type of a specific event.\n *\n * @example\n * ```typescript\n * // Extract detail type for specific event\n * type ClickDetail = DataGridEventDetail<'cell-click', Employee>;\n * // Equivalent to: CellClickDetail<Employee>\n *\n * // Use in generic handler\n * function logDetail<K extends keyof DataGridEventMap>(\n * eventName: K,\n * detail: DataGridEventDetail<K>,\n * ): void {\n * console.log(`${eventName}:`, detail);\n * }\n * ```\n *\n * @see {@link DataGridEventMap} for all event types\n * @category Events\n */\nexport type DataGridEventDetail<K extends keyof DataGridEventMap<unknown>, TRow = unknown> = DataGridEventMap<TRow>[K];\n\n/**\n * Custom event type for DataGrid events with typed detail payload.\n *\n * Use this type when you need to cast or declare event handler parameters.\n *\n * @example\n * ```typescript\n * // Strongly typed event handler\n * function onCellClick(e: DataGridCustomEvent<'cell-click', Employee>): void {\n * const { row, field, value } = e.detail;\n * console.log(`Clicked ${field} = ${value} on ${row.name}`);\n * }\n *\n * grid.addEventListener('cell-click', onCellClick);\n * ```\n *\n * @see {@link DataGridEventMap} for all event types\n * @see {@link DataGridEventDetail} for extracting detail type only\n * @category Events\n */\nexport type DataGridCustomEvent<K extends keyof DataGridEventMap<unknown>, TRow = unknown> = CustomEvent<\n DataGridEventMap<TRow>[K]\n>;\n\n/**\n * Template evaluation context for dynamic templates.\n *\n * @category Plugin Development\n */\nexport interface EvalContext {\n value: unknown;\n row: Record<string, unknown> | null;\n}\n// #endregion\n","/**\n * Base Grid Plugin Class\n *\n * All plugins extend this abstract class.\n * Plugins are instantiated per-grid and manage their own state.\n */\n\n// Injected by Vite at build time from package.json (same as grid.ts)\ndeclare const __GRID_VERSION__: string;\n\nimport type {\n ColumnConfig,\n ColumnState,\n GridPlugin,\n HeaderContentDefinition,\n IconValue,\n ToolPanelDefinition,\n} from '../types';\nimport { DEFAULT_GRID_ICONS } from '../types';\n\n// Re-export shared plugin types for convenience\nexport { PLUGIN_QUERIES } from './types';\nexport type {\n AfterCellRenderContext,\n AfterRowRenderContext,\n CellClickEvent,\n CellCoords,\n CellEditor,\n CellMouseEvent,\n CellRenderer,\n ContextMenuItem,\n ContextMenuParams,\n GridElementRef,\n HeaderClickEvent,\n HeaderRenderer,\n KeyboardModifiers,\n PluginCellRenderContext,\n PluginQuery,\n RowClickEvent,\n ScrollEvent,\n} from './types';\n\nimport type {\n AfterCellRenderContext,\n AfterRowRenderContext,\n CellClickEvent,\n CellEditor,\n CellMouseEvent,\n CellRenderer,\n GridElementRef,\n HeaderClickEvent,\n HeaderRenderer,\n PluginQuery,\n RowClickEvent,\n ScrollEvent,\n} from './types';\n\n/**\n * Grid element interface for plugins.\n * Extends GridElementRef with plugin-specific methods.\n * Note: effectiveConfig is already available from GridElementRef.\n */\nexport interface GridElement extends GridElementRef {\n getPlugin<T extends BaseGridPlugin>(PluginClass: new (...args: any[]) => T): T | undefined;\n getPluginByName(name: string): BaseGridPlugin | undefined;\n /**\n * Get a plugin's state by plugin name.\n * This is a loose-coupling method for plugins to access other plugins' state\n * without importing the plugin class directly.\n * @internal Plugin API\n */\n getPluginState?(name: string): unknown;\n}\n\n/**\n * Header render context for plugin header renderers.\n */\nexport interface PluginHeaderRenderContext {\n /** Column configuration */\n column: ColumnConfig;\n /** Column index */\n colIndex: number;\n}\n\n// ============================================================================\n// Plugin Dependency Types\n// ============================================================================\n\n/**\n * Declares a dependency on another plugin.\n *\n * @category Plugin Development\n * @example\n * ```typescript\n * export class UndoRedoPlugin extends BaseGridPlugin {\n * static readonly dependencies: PluginDependency[] = [\n * { name: 'editing', required: true },\n * ];\n * }\n * ```\n */\nexport interface PluginDependency {\n /**\n * The name of the required plugin (matches the plugin's `name` property).\n * Use string names for loose coupling - avoids static imports.\n */\n name: string;\n\n /**\n * Whether this dependency is required (hard) or optional (soft).\n * - `true`: Plugin cannot function without it. Throws error if missing.\n * - `false`: Plugin works with reduced functionality. Logs info if missing.\n * @default true\n */\n required?: boolean;\n\n /**\n * Human-readable reason for this dependency.\n * Shown in error/info messages to help users understand why.\n * @example \"UndoRedoPlugin needs EditingPlugin to track cell edits\"\n */\n reason?: string;\n}\n\n/**\n * Declares an incompatibility between plugins.\n * When both plugins are loaded, a warning is logged to help users understand the conflict.\n *\n * @category Plugin Development\n */\nexport interface PluginIncompatibility {\n /**\n * The name of the incompatible plugin (matches the plugin's `name` property).\n */\n name: string;\n\n /**\n * Human-readable reason for the incompatibility.\n * Should explain why the plugins conflict and any workarounds.\n * @example \"Responsive card layout does not support row grouping yet\"\n */\n reason: string;\n}\n\n// ============================================================================\n// Plugin Query & Event Definitions\n// ============================================================================\n\n/**\n * Defines a query that a plugin can handle.\n * Other plugins or the grid can send this query type to retrieve data.\n *\n * @category Plugin Development\n *\n * @example\n * ```typescript\n * // In manifest\n * queries: [\n * {\n * type: 'canMoveColumn',\n * description: 'Check if a column can be moved/reordered',\n * },\n * ]\n *\n * // In plugin class\n * handleQuery(query: PluginQuery): unknown {\n * if (query.type === 'canMoveColumn') {\n * return this.canMoveColumn(query.context);\n * }\n * }\n * ```\n */\nexport interface QueryDefinition {\n /**\n * The query type identifier (e.g., 'canMoveColumn', 'getContextMenuItems').\n * Should be unique across all plugins.\n */\n type: string;\n\n /**\n * Human-readable description of what the query does.\n */\n description?: string;\n}\n\n/**\n * Defines an event that a plugin can emit.\n * Other plugins can subscribe to these events via the Event Bus.\n *\n * @category Plugin Development\n *\n * @example\n * ```typescript\n * // In manifest\n * events: [\n * {\n * type: 'filter-change',\n * description: 'Emitted when filter criteria change',\n * },\n * ]\n *\n * // In plugin class - emit\n * this.emitPluginEvent('filter-change', { field: 'name', value: 'Alice' });\n *\n * // In another plugin - subscribe\n * this.on('filter-change', (detail) => console.log('Filter changed:', detail));\n * ```\n */\nexport interface EventDefinition {\n /**\n * The event type identifier (e.g., 'filter-change', 'selection-change').\n * Used when emitting and subscribing to events.\n */\n type: string;\n\n /**\n * Human-readable description of what the event represents.\n */\n description?: string;\n\n /**\n * Whether this event is cancelable via `event.preventDefault()`.\n * @default false\n */\n cancelable?: boolean;\n}\n\n// ============================================================================\n// Plugin Manifest Types\n// ============================================================================\n\n/**\n * Defines a property that a plugin \"owns\" - used for configuration validation.\n * When this property is used without the owning plugin loaded, an error is thrown.\n *\n * @category Plugin Development\n */\nexport interface PluginPropertyDefinition {\n /** The property name on column or grid config */\n property: string;\n /** Whether this is a column-level or config-level property */\n level: 'column' | 'config';\n /** Human-readable description for error messages (e.g., 'the \"editable\" column property') */\n description: string;\n /** Import path hint for error messages (e.g., \"import { EditingPlugin } from '@toolbox-web/grid/plugins/editing';\") */\n importHint?: string;\n /** Custom check for whether property is considered \"used\" (default: truthy value check) */\n isUsed?: (value: unknown) => boolean;\n}\n\n/**\n * A configuration validation rule for detecting invalid/conflicting settings.\n * Plugins declare rules in their manifest; the validator executes them during grid initialization.\n *\n * @category Plugin Development\n * @template TConfig - The plugin's configuration type\n */\nexport interface PluginConfigRule<TConfig = unknown> {\n /** Rule identifier for debugging (e.g., 'selection/range-dblclick') */\n id: string;\n /** Severity: 'error' throws, 'warn' logs warning */\n severity: 'error' | 'warn';\n /** Human-readable message shown when rule is violated */\n message: string;\n /** Predicate returning true if the rule is VIOLATED (i.e., config is invalid) */\n check: (pluginConfig: TConfig) => boolean;\n}\n\n/**\n * Hook names that can have execution priority configured.\n *\n * @category Plugin Development\n */\nexport type HookName =\n | 'processColumns'\n | 'processRows'\n | 'afterRender'\n | 'afterCellRender'\n | 'afterRowRender'\n | 'onCellClick'\n | 'onCellMouseDown'\n | 'onCellMouseMove'\n | 'onCellMouseUp'\n | 'onKeyDown'\n | 'onScroll'\n | 'onScrollRender';\n\n/**\n * Static metadata about a plugin's capabilities and requirements.\n * Declared as a static property on plugin classes.\n *\n * @category Plugin Development\n * @template TConfig - The plugin's configuration type\n *\n * @example\n * ```typescript\n * export class MyPlugin extends BaseGridPlugin<MyConfig> {\n * static override readonly manifest: PluginManifest<MyConfig> = {\n * ownedProperties: [\n * { property: 'myProp', level: 'column', description: 'the \"myProp\" column property' },\n * ],\n * configRules: [\n * { id: 'my-plugin/invalid-combo', severity: 'warn', message: '...', check: (c) => c.a && c.b },\n * ],\n * };\n * readonly name = 'myPlugin';\n * }\n * ```\n */\nexport interface PluginManifest<TConfig = unknown> {\n /**\n * Properties this plugin owns - validated by validate-config.ts.\n * If a user uses one of these properties without loading the plugin, an error is thrown.\n */\n ownedProperties?: PluginPropertyDefinition[];\n\n /**\n * Hook execution priority (higher = later, default 0).\n * Use negative values to run earlier, positive to run later.\n */\n hookPriority?: Partial<Record<HookName, number>>;\n\n /**\n * Configuration validation rules - checked during grid initialization.\n * Rules with severity 'error' throw, 'warn' logs to console.\n */\n configRules?: PluginConfigRule<TConfig>[];\n\n /**\n * Plugins that are incompatible with this plugin.\n * When both plugins are loaded together, a warning is shown.\n *\n * @example\n * ```typescript\n * incompatibleWith: [\n * { name: 'groupingRows', reason: 'Responsive card layout does not support row grouping yet' },\n * ],\n * ```\n */\n incompatibleWith?: PluginIncompatibility[];\n\n /**\n * Queries this plugin can handle.\n * Declares what query types this plugin responds to via `handleQuery()`.\n * This replaces the centralized PLUGIN_QUERIES approach with manifest-declared queries.\n *\n * @example\n * ```typescript\n * queries: [\n * { type: 'canMoveColumn', description: 'Check if a column can be moved' },\n * ],\n * ```\n */\n queries?: QueryDefinition[];\n\n /**\n * Events this plugin can emit.\n * Declares what event types other plugins can subscribe to via `on()`.\n *\n * @example\n * ```typescript\n * events: [\n * { type: 'filter-change', description: 'Emitted when filter criteria change' },\n * ],\n * ```\n */\n events?: EventDefinition[];\n}\n\n/**\n * Abstract base class for all grid plugins.\n *\n * @category Plugin Development\n * @template TConfig - Configuration type for the plugin\n */\nexport abstract class BaseGridPlugin<TConfig = unknown> implements GridPlugin {\n /**\n * Plugin dependencies - declare other plugins this one requires.\n *\n * Dependencies are validated when the plugin is attached.\n * Required dependencies throw an error if missing.\n * Optional dependencies log an info message if missing.\n *\n * @example\n * ```typescript\n * static readonly dependencies: PluginDependency[] = [\n * { name: 'editing', required: true, reason: 'Tracks cell edits for undo/redo' },\n * { name: 'selection', required: false, reason: 'Enables selection-based undo' },\n * ];\n * ```\n */\n static readonly dependencies?: PluginDependency[];\n\n /**\n * Plugin manifest - declares owned properties, config rules, and hook priorities.\n *\n * This is read by the configuration validator to:\n * - Validate that required plugins are loaded when their properties are used\n * - Execute configRules to detect invalid/conflicting settings\n * - Order hook execution based on priority\n *\n * @example\n * ```typescript\n * static override readonly manifest: PluginManifest<MyConfig> = {\n * ownedProperties: [\n * { property: 'myProp', level: 'column', description: 'the \"myProp\" column property' },\n * ],\n * configRules: [\n * { id: 'myPlugin/conflict', severity: 'warn', message: '...', check: (c) => c.a && c.b },\n * ],\n * };\n * ```\n */\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n static readonly manifest?: PluginManifest<any>;\n\n /** Unique plugin identifier (derived from class name by default) */\n abstract readonly name: string;\n\n /**\n * Plugin version - defaults to grid version for built-in plugins.\n * Third-party plugins can override with their own semver.\n */\n readonly version: string = typeof __GRID_VERSION__ !== 'undefined' ? __GRID_VERSION__ : 'dev';\n\n /** CSS styles to inject into the grid's shadow DOM */\n readonly styles?: string;\n\n /** Custom cell renderers keyed by type name */\n readonly cellRenderers?: Record<string, CellRenderer>;\n\n /** Custom header renderers keyed by type name */\n readonly headerRenderers?: Record<string, HeaderRenderer>;\n\n /** Custom cell editors keyed by type name */\n readonly cellEditors?: Record<string, CellEditor>;\n\n /** The grid instance this plugin is attached to */\n protected grid!: GridElement;\n\n /** Plugin configuration - merged with defaults in attach() */\n protected config!: TConfig;\n\n /** User-provided configuration from constructor */\n protected readonly userConfig: Partial<TConfig>;\n\n /**\n * Plugin-level AbortController for event listener cleanup.\n * Created fresh in attach(), aborted in detach().\n * This ensures event listeners are properly cleaned up when plugins are re-attached.\n */\n #abortController?: AbortController;\n\n /**\n * Default configuration - subclasses should override this getter.\n * Note: This must be a getter (not property initializer) for proper inheritance\n * since property initializers run after parent constructor.\n */\n protected get defaultConfig(): Partial<TConfig> {\n return {};\n }\n\n constructor(config: Partial<TConfig> = {}) {\n this.userConfig = config;\n }\n\n /**\n * Called when the plugin is attached to a grid.\n * Override to set up event listeners, initialize state, etc.\n *\n * @example\n * ```ts\n * attach(grid: GridElement): void {\n * super.attach(grid);\n * // Set up document-level listeners with auto-cleanup\n * document.addEventListener('keydown', this.handleEscape, {\n * signal: this.disconnectSignal\n * });\n * }\n * ```\n */\n attach(grid: GridElement): void {\n // Abort any previous abort controller (in case of re-attach without detach)\n this.#abortController?.abort();\n // Create fresh abort controller for this attachment\n this.#abortController = new AbortController();\n\n this.grid = grid;\n // Merge config here (after subclass construction is complete)\n this.config = { ...this.defaultConfig, ...this.userConfig } as TConfig;\n }\n\n /**\n * Called when the plugin is detached from a grid.\n * Override to clean up event listeners, timers, etc.\n *\n * @example\n * ```ts\n * detach(): void {\n * // Clean up any state not handled by disconnectSignal\n * this.selectedRows.clear();\n * this.cache = null;\n * }\n * ```\n */\n detach(): void {\n // Abort the plugin's abort controller to clean up all event listeners\n // registered with { signal: this.disconnectSignal }\n this.#abortController?.abort();\n this.#abortController = undefined;\n // Override in subclass for additional cleanup\n }\n\n /**\n * Called when another plugin is attached to the same grid.\n * Use for inter-plugin coordination, e.g., to integrate with new plugins.\n *\n * @param name - The name of the plugin that was attached\n * @param plugin - The plugin instance (for direct access if needed)\n *\n * @example\n * ```ts\n * onPluginAttached(name: string, plugin: BaseGridPlugin): void {\n * if (name === 'selection') {\n * // Integrate with selection plugin\n * this.selectionPlugin = plugin as SelectionPlugin;\n * }\n * }\n * ```\n */\n onPluginAttached?(name: string, plugin: BaseGridPlugin): void;\n\n /**\n * Called when another plugin is detached from the same grid.\n * Use to clean up inter-plugin references.\n *\n * @param name - The name of the plugin that was detached\n *\n * @example\n * ```ts\n * onPluginDetached(name: string): void {\n * if (name === 'selection') {\n * this.selectionPlugin = undefined;\n * }\n * }\n * ```\n */\n onPluginDetached?(name: string): void;\n\n /**\n * Get another plugin instance from the same grid.\n * Use for inter-plugin communication.\n *\n * @example\n * ```ts\n * const selection = this.getPlugin(SelectionPlugin);\n * if (selection) {\n * const selectedRows = selection.getSelectedRows();\n * }\n * ```\n */\n protected getPlugin<T extends BaseGridPlugin>(PluginClass: new (...args: any[]) => T): T | undefined {\n return this.grid?.getPlugin(PluginClass);\n }\n\n /**\n * Emit a custom event from the grid.\n */\n protected emit<T>(eventName: string, detail: T): void {\n this.grid?.dispatchEvent?.(new CustomEvent(eventName, { detail, bubbles: true }));\n }\n\n /**\n * Emit a cancelable custom event from the grid.\n * @returns `true` if the event was cancelled (preventDefault called), `false` otherwise\n */\n protected emitCancelable<T>(eventName: string, detail: T): boolean {\n const event = new CustomEvent(eventName, { detail, bubbles: true, cancelable: true });\n this.grid?.dispatchEvent?.(event);\n return event.defaultPrevented;\n }\n\n // =========================================================================\n // Event Bus - Plugin-to-Plugin Communication\n // =========================================================================\n\n /**\n * Subscribe to an event from another plugin.\n * The subscription is automatically cleaned up when this plugin is detached.\n *\n * @category Plugin Development\n * @param eventType - The event type to listen for (e.g., 'filter-change')\n * @param callback - The callback to invoke when the event is emitted\n *\n * @example\n * ```typescript\n * // In attach() or other initialization\n * this.on('filter-change', (detail) => {\n * console.log('Filter changed:', detail);\n * });\n * ```\n */\n protected on<T = unknown>(eventType: string, callback: (detail: T) => void): void {\n this.grid?._pluginManager?.subscribe(this, eventType, callback as (detail: unknown) => void);\n }\n\n /**\n * Unsubscribe from a plugin event.\n *\n * @category Plugin Development\n * @param eventType - The event type to stop listening for\n *\n * @example\n * ```typescript\n * this.off('filter-change');\n * ```\n */\n protected off(eventType: string): void {\n this.grid?._pluginManager?.unsubscribe(this, eventType);\n }\n\n /**\n * Emit an event to other plugins via the Event Bus.\n * This is for inter-plugin communication only; it does NOT dispatch DOM events.\n * Use `emit()` to dispatch DOM events that external consumers can listen to.\n *\n * @category Plugin Development\n * @param eventType - The event type to emit (should be declared in manifest.events)\n * @param detail - The event payload\n *\n * @example\n * ```typescript\n * // Emit to other plugins (not DOM)\n * this.emitPluginEvent('filter-change', { field: 'name', value: 'Alice' });\n *\n * // For DOM events that consumers can addEventListener to:\n * this.emit('filter-change', { field: 'name', value: 'Alice' });\n * ```\n */\n protected emitPluginEvent<T>(eventType: string, detail: T): void {\n this.grid?._pluginManager?.emitPluginEvent(eventType, detail);\n }\n\n /**\n * Request a re-render of the grid.\n * Uses ROWS phase - does NOT trigger processColumns hooks.\n */\n protected requestRender(): void {\n this.grid?.requestRender?.();\n }\n\n /**\n * Request a columns re-render of the grid.\n * Uses COLUMNS phase - triggers processColumns hooks.\n * Use this when your plugin needs to reprocess column configuration.\n */\n protected requestColumnsRender(): void {\n (this.grid as { requestColumnsRender?: () => void })?.requestColumnsRender?.();\n }\n\n /**\n * Request a re-render and restore focus styling afterward.\n * Use this when a plugin action (like expand/collapse) triggers a render\n * but needs to maintain keyboard navigation focus.\n */\n protected requestRenderWithFocus(): void {\n this.grid?.requestRenderWithFocus?.();\n }\n\n /**\n * Request a lightweight style update without rebuilding DOM.\n * Use this instead of requestRender() when only CSS classes need updating.\n */\n protected requestAfterRender(): void {\n this.grid?.requestAfterRender?.();\n }\n\n /**\n * Get the current rows from the grid.\n */\n protected get rows(): any[] {\n return this.grid?.rows ?? [];\n }\n\n /**\n * Get the original unfiltered/unprocessed rows from the grid.\n * Use this when you need all source data regardless of active filters.\n */\n protected get sourceRows(): any[] {\n return this.grid?.sourceRows ?? [];\n }\n\n /**\n * Get the current columns from the grid.\n */\n protected get columns(): ColumnConfig[] {\n return this.grid?.columns ?? [];\n }\n\n /**\n * Get only visible columns from the grid (excludes hidden).\n * Use this for rendering that needs to match the grid template.\n */\n protected get visibleColumns(): ColumnConfig[] {\n return this.grid?._visibleColumns ?? [];\n }\n\n /**\n * Get the grid as an HTMLElement for direct DOM operations.\n * Use sparingly - prefer the typed GridElementRef API when possible.\n *\n * @example\n * ```ts\n * const width = this.gridElement.clientWidth;\n * this.gridElement.classList.add('my-plugin-active');\n * ```\n */\n protected get gridElement(): HTMLElement {\n return this.grid as unknown as HTMLElement;\n }\n\n /**\n * Get the disconnect signal for event listener cleanup.\n * This signal is aborted when the grid disconnects from the DOM.\n * Use this when adding event listeners that should be cleaned up automatically.\n *\n * Best for:\n * - Document/window-level listeners added in attach()\n * - Listeners on the grid element itself\n * - Any listener that should persist across renders\n *\n * Not needed for:\n * - Listeners on elements created in afterRender() (removed with element)\n *\n * @example\n * element.addEventListener('click', handler, { signal: this.disconnectSignal });\n * document.addEventListener('keydown', handler, { signal: this.disconnectSignal });\n */\n protected get disconnectSignal(): AbortSignal {\n // Return the plugin's own abort signal for proper cleanup on detach/re-attach\n // Falls back to grid's signal if plugin's controller not yet created\n return this.#abortController?.signal ?? this.grid?.disconnectSignal;\n }\n\n /**\n * Get the grid-level icons configuration.\n * Returns merged icons (user config + defaults).\n */\n protected get gridIcons(): typeof DEFAULT_GRID_ICONS {\n const userIcons = this.grid?.gridConfig?.icons ?? {};\n return { ...DEFAULT_GRID_ICONS, ...userIcons };\n }\n\n // #region Animation Helpers\n\n /**\n * Check if animations are enabled at the grid level.\n * Respects gridConfig.animation.mode and the CSS variable set by the grid.\n *\n * Plugins should use this to skip animations when:\n * - Animation mode is 'off' or `false`\n * - User prefers reduced motion and mode is 'reduced-motion' (default)\n *\n * @example\n * ```ts\n * private get animationStyle(): 'slide' | 'fade' | false {\n * if (!this.isAnimationEnabled) return false;\n * return this.config.animation ?? 'slide';\n * }\n * ```\n */\n protected get isAnimationEnabled(): boolean {\n const mode = this.grid?.effectiveConfig?.animation?.mode ?? 'reduced-motion';\n\n // Explicit off = disabled\n if (mode === false || mode === 'off') return false;\n\n // Explicit on = always enabled\n if (mode === true || mode === 'on') return true;\n\n // reduced-motion: check CSS variable (set by grid based on media query)\n const host = this.gridElement;\n if (host) {\n const enabled = getComputedStyle(host).getPropertyValue('--tbw-animation-enabled').trim();\n return enabled !== '0';\n }\n\n return true; // Default to enabled\n }\n\n /**\n * Get the animation duration in milliseconds from CSS variable.\n * Falls back to 200ms if not set.\n *\n * Plugins can use this for their animation timing to stay consistent\n * with the grid-level animation.duration setting.\n *\n * @example\n * ```ts\n * element.animate(keyframes, { duration: this.animationDuration });\n * ```\n */\n protected get animationDuration(): number {\n const host = this.gridElement;\n if (host) {\n const durationStr = getComputedStyle(host).getPropertyValue('--tbw-animation-duration').trim();\n const parsed = parseInt(durationStr, 10);\n if (!isNaN(parsed)) return parsed;\n }\n return 200; // Default\n }\n\n // #endregion\n\n /**\n * Resolve an icon value to string or HTMLElement.\n * Checks plugin config first, then grid-level icons, then defaults.\n *\n * @param iconKey - The icon key in GridIcons (e.g., 'expand', 'collapse')\n * @param pluginOverride - Optional plugin-level override\n * @returns The resolved icon value\n */\n protected resolveIcon(iconKey: keyof typeof DEFAULT_GRID_ICONS, pluginOverride?: IconValue): IconValue {\n // Plugin override takes precedence\n if (pluginOverride !== undefined) {\n return pluginOverride;\n }\n // Then grid-level config\n return this.gridIcons[iconKey];\n }\n\n /**\n * Set an icon value on an element.\n * Handles both string (text/HTML) and HTMLElement values.\n *\n * @param element - The element to set the icon on\n * @param icon - The icon value (string or HTMLElement)\n */\n protected setIcon(element: HTMLElement, icon: IconValue): void {\n if (typeof icon === 'string') {\n element.innerHTML = icon;\n } else if (icon instanceof HTMLElement) {\n element.innerHTML = '';\n element.appendChild(icon.cloneNode(true));\n }\n }\n\n /**\n * Log a warning message.\n */\n protected warn(message: string): void {\n console.warn(`[tbw-grid:${this.name}] ${message}`);\n }\n\n // #region Lifecycle Hooks\n\n /**\n * Transform rows before rendering.\n * Called during each render cycle before rows are rendered to the DOM.\n * Use this to filter, sort, or add computed properties to rows.\n *\n * @param rows - The current rows array (readonly to encourage returning a new array)\n * @returns The modified rows array to render\n *\n * @example\n * ```ts\n * processRows(rows: readonly any[]): any[] {\n * // Filter out hidden rows\n * return rows.filter(row => !row._hidden);\n * }\n * ```\n *\n * @example\n * ```ts\n * processRows(rows: readonly any[]): any[] {\n * // Add computed properties\n * return rows.map(row => ({\n * ...row,\n * _fullName: `${row.firstName} ${row.lastName}`\n * }));\n * }\n * ```\n */\n processRows?(rows: readonly any[]): any[];\n\n /**\n * Transform columns before rendering.\n * Called during each render cycle before column headers and cells are rendered.\n * Use this to add, remove, or modify column definitions.\n *\n * @param columns - The current columns array (readonly to encourage returning a new array)\n * @returns The modified columns array to render\n *\n * @example\n * ```ts\n * processColumns(columns: readonly ColumnConfig[]): ColumnConfig[] {\n * // Add a selection checkbox column\n * return [\n * { field: '_select', header: '', width: 40 },\n * ...columns\n * ];\n * }\n * ```\n */\n processColumns?(columns: readonly ColumnConfig[]): ColumnConfig[];\n\n /**\n * Called before each render cycle begins.\n * Use this to prepare state or cache values needed during rendering.\n *\n * **Note:** This hook is currently a placeholder for future implementation.\n * It is defined in the interface but not yet invoked by the grid's render pipeline.\n * If you need this functionality, please open an issue or contribute an implementation.\n *\n * @example\n * ```ts\n * beforeRender(): void {\n * this.visibleRowCount = this.calculateVisibleRows();\n * }\n * ```\n */\n beforeRender?(): void;\n\n /**\n * Called after each render cycle completes.\n * Use this for DOM manipulation, adding event listeners to rendered elements,\n * or applying visual effects like selection highlights.\n *\n * @example\n * ```ts\n * afterRender(): void {\n * // Apply selection styling to rendered rows\n * const rows = this.gridElement?.querySelectorAll('.data-row');\n * rows?.forEach((row, i) => {\n * row.classList.toggle('selected', this.selectedRows.has(i));\n * });\n * }\n * ```\n */\n afterRender?(): void;\n\n /**\n * Called after each cell is rendered.\n * This hook is more efficient than `afterRender()` for cell-level modifications\n * because you receive the cell context directly - no DOM queries needed.\n *\n * Use cases:\n * - Adding selection/highlight classes to specific cells\n * - Injecting badges or decorations\n * - Applying conditional styling based on cell value\n *\n * Performance note: Called for every visible cell during render. Keep implementation fast.\n * This hook is also called during scroll when cells are recycled.\n *\n * @param context - The cell render context with row, column, value, and elements\n *\n * @example\n * ```ts\n * afterCellRender(context: AfterCellRenderContext): void {\n * // Add selection class without DOM queries\n * if (this.isSelected(context.rowIndex, context.colIndex)) {\n * context.cellElement.classList.add('selected');\n * }\n *\n * // Add validation error styling\n * if (this.hasError(context.row, context.column.field)) {\n * context.cellElement.classList.add('has-error');\n * }\n * }\n * ```\n */\n afterCellRender?(context: AfterCellRenderContext): void;\n\n /**\n * Called after a row is fully rendered (all cells complete).\n * Use this for row-level decorations, styling, or ARIA attributes.\n *\n * Common use cases:\n * - Adding selection classes to entire rows (row-focus, selected)\n * - Setting row-level ARIA attributes\n * - Applying row validation highlighting\n * - Tree indentation styling\n *\n * Performance note: Called for every visible row during render. Keep implementation fast.\n * This hook is also called during scroll when rows are recycled.\n *\n * @param context - The row render context with row data and element\n *\n * @example\n * ```ts\n * afterRowRender(context: AfterRowRenderContext): void {\n * // Add row selection class without DOM queries\n * if (this.isRowSelected(context.rowIndex)) {\n * context.rowElement.classList.add('selected', 'row-focus');\n * }\n *\n * // Add validation error styling\n * if (this.rowHasErrors(context.row)) {\n * context.rowElement.classList.add('has-errors');\n * }\n * }\n * ```\n */\n afterRowRender?(context: AfterRowRenderContext): void;\n\n /**\n * Called after scroll-triggered row rendering completes.\n * This is a lightweight hook for applying visual state to recycled DOM elements.\n * Use this instead of afterRender when you need to reapply styling during scroll.\n *\n * Performance note: This is called frequently during scroll. Keep implementation fast.\n *\n * @example\n * ```ts\n * onScrollRender(): void {\n * // Reapply selection state to visible cells\n * this.applySelectionToVisibleCells();\n * }\n * ```\n */\n onScrollRender?(): void;\n\n /**\n * Return extra height contributed by this plugin (e.g., expanded detail rows).\n * Used to adjust scrollbar height calculations for virtualization.\n *\n * @returns Total extra height in pixels\n *\n * @deprecated Use {@link getRowHeight} instead. This hook will be removed in v3.0.\n * The new `getRowHeight(row, index)` hook provides per-row height information which\n * enables better position caching and variable row height support.\n *\n * @example\n * ```ts\n * // OLD (deprecated):\n * getExtraHeight(): number {\n * return this.expandedRows.size * this.detailHeight;\n * }\n *\n * // NEW (preferred):\n * getRowHeight(row: unknown, index: number): number | undefined {\n * if (this.isExpanded(row)) {\n * return this.baseRowHeight + this.getDetailHeight(row);\n * }\n * return undefined;\n * }\n * ```\n */\n getExtraHeight?(): number;\n\n /**\n * Return extra height that appears before a given row index.\n * Used by virtualization to correctly calculate scroll positions when\n * there's variable height content (like expanded detail rows) above the viewport.\n *\n * @param beforeRowIndex - The row index to calculate extra height before\n * @returns Extra height in pixels that appears before this row\n *\n * @deprecated Use {@link getRowHeight} instead. This hook will be removed in v3.0.\n * The new `getRowHeight(row, index)` hook provides per-row height information which\n * enables better position caching and variable row height support.\n *\n * @example\n * ```ts\n * // OLD (deprecated):\n * getExtraHeightBefore(beforeRowIndex: number): number {\n * let height = 0;\n * for (const expandedRowIndex of this.expandedRowIndices) {\n * if (expandedRowIndex < beforeRowIndex) {\n * height += this.getDetailHeight(expandedRowIndex);\n * }\n * }\n * return height;\n * }\n *\n * // NEW (preferred):\n * getRowHeight(row: unknown, index: number): number | undefined {\n * if (this.isExpanded(row)) {\n * return this.baseRowHeight + this.getDetailHeight(row);\n * }\n * return undefined;\n * }\n * ```\n */\n getExtraHeightBefore?(beforeRowIndex: number): number;\n\n /**\n * Get the height of a specific row.\n * Used for synthetic rows (group headers, detail panels, etc.) that have fixed heights.\n * Return undefined if this plugin does not manage the height for this row.\n *\n * This hook is called during position cache rebuilds for variable row height virtualization.\n * Plugins that create synthetic rows should implement this to provide accurate heights.\n *\n * @param row - The row data\n * @param index - The row index in the processed rows array\n * @returns The row height in pixels, or undefined if not managed by this plugin\n *\n * @example\n * ```ts\n * getRowHeight(row: unknown, index: number): number | undefined {\n * // Group headers have a fixed height\n * if (this.isGroupHeader(row)) {\n * return 32;\n * }\n * return undefined; // Let grid use default/measured height\n * }\n * ```\n */\n getRowHeight?(row: unknown, index: number): number | undefined;\n\n /**\n * Adjust the virtualization start index to render additional rows before the visible range.\n * Use this when expanded content (like detail rows) needs its parent row to remain rendered\n * even when the parent row itself has scrolled above the viewport.\n *\n * @param start - The calculated start row index\n * @param scrollTop - The current scroll position\n * @param rowHeight - The height of a single row\n * @returns The adjusted start index (lower than or equal to original start)\n *\n * @example\n * ```ts\n * adjustVirtualStart(start: number, scrollTop: number, rowHeight: number): number {\n * // If row 5 is expanded and scrolled partially, keep it rendered\n * for (const expandedRowIndex of this.expandedRowIndices) {\n * const expandedRowTop = expandedRowIndex * rowHeight;\n * const expandedRowBottom = expandedRowTop + rowHeight + this.detailHeight;\n * if (expandedRowBottom > scrollTop && expandedRowIndex < start) {\n * return expandedRowIndex;\n * }\n * }\n * return start;\n * }\n * ```\n */\n adjustVirtualStart?(start: number, scrollTop: number, rowHeight: number): number;\n\n /**\n * Render a custom row, bypassing the default row rendering.\n * Use this for special row types like group headers, detail rows, or footers.\n *\n * @param row - The row data object\n * @param rowEl - The row DOM element to render into\n * @param rowIndex - The index of the row in the data array\n * @returns `true` if the plugin handled rendering (prevents default), `false`/`void` for default rendering\n *\n * @example\n * ```ts\n * renderRow(row: any, rowEl: HTMLElement, rowIndex: number): boolean | void {\n * if (row._isGroupHeader) {\n * rowEl.innerHTML = `<div class=\"group-header\">${row._groupLabel}</div>`;\n * return true; // Handled - skip default rendering\n * }\n * // Return void to let default rendering proceed\n * }\n * ```\n */\n renderRow?(row: any, rowEl: HTMLElement, rowIndex: number): boolean | void;\n\n // #endregion\n\n // #region Inter-Plugin Communication\n\n /**\n * Handle queries from other plugins.\n * This is the generic mechanism for inter-plugin communication.\n * Plugins can respond to well-known query types or define their own.\n *\n * **Prefer `handleQuery` for new plugins** - it has the same signature but\n * a clearer name. `onPluginQuery` is kept for backwards compatibility.\n *\n * @category Plugin Development\n * @param query - The query object with type and context\n * @returns Query-specific response, or undefined if not handling this query\n *\n * @example\n * ```ts\n * onPluginQuery(query: PluginQuery): unknown {\n * switch (query.type) {\n * case PLUGIN_QUERIES.CAN_MOVE_COLUMN:\n * // Prevent moving pinned columns\n * const column = query.context as ColumnConfig;\n * if (column.sticky === 'left' || column.sticky === 'right') {\n * return false;\n * }\n * break;\n * case PLUGIN_QUERIES.GET_CONTEXT_MENU_ITEMS:\n * const params = query.context as ContextMenuParams;\n * return [{ id: 'my-action', label: 'My Action', action: () => {} }];\n * }\n * }\n * ```\n * @deprecated Use `handleQuery` instead for new plugins.\n */\n onPluginQuery?(query: PluginQuery): unknown;\n\n /**\n * Handle queries from other plugins or the grid.\n *\n * Queries are declared in `manifest.queries` and dispatched via `grid.query()`.\n * This enables type-safe, discoverable inter-plugin communication.\n *\n * @category Plugin Development\n * @param query - The query object with type and context\n * @returns Query-specific response, or undefined if not handling this query\n *\n * @example\n * ```ts\n * // In manifest\n * static override readonly manifest: PluginManifest = {\n * queries: [\n * { type: 'canMoveColumn', description: 'Check if a column can be moved' },\n * ],\n * };\n *\n * // In plugin class\n * handleQuery(query: PluginQuery): unknown {\n * if (query.type === 'canMoveColumn') {\n * const column = query.context as ColumnConfig;\n * return !column.sticky; // Can't move sticky columns\n * }\n * }\n * ```\n */\n handleQuery?(query: PluginQuery): unknown;\n\n // #endregion\n\n // #region Interaction Hooks\n\n /**\n * Handle keyboard events on the grid.\n * Called when a key is pressed while the grid or a cell has focus.\n *\n * @param event - The native KeyboardEvent\n * @returns `true` to prevent default behavior and stop propagation, `false`/`void` to allow default\n *\n * @example\n * ```ts\n * onKeyDown(event: KeyboardEvent): boolean | void {\n * // Handle Ctrl+A for select all\n * if (event.ctrlKey && event.key === 'a') {\n * this.selectAllRows();\n * return true; // Prevent default browser select-all\n * }\n * }\n * ```\n */\n onKeyDown?(event: KeyboardEvent): boolean | void;\n\n /**\n * Handle cell click events.\n * Called when a data cell is clicked (not headers).\n *\n * @param event - Cell click event with row/column context\n * @returns `true` to prevent default behavior and stop propagation, `false`/`void` to allow default\n *\n * @example\n * ```ts\n * onCellClick(event: CellClickEvent): boolean | void {\n * if (event.field === '_select') {\n * this.toggleRowSelection(event.rowIndex);\n * return true; // Handled\n * }\n * }\n * ```\n */\n onCellClick?(event: CellClickEvent): boolean | void;\n\n /**\n * Handle row click events.\n * Called when any part of a data row is clicked.\n * Note: This is called in addition to onCellClick, not instead of.\n *\n * @param event - Row click event with row context\n * @returns `true` to prevent default behavior and stop propagation, `false`/`void` to allow default\n *\n * @example\n * ```ts\n * onRowClick(event: RowClickEvent): boolean | void {\n * if (this.config.mode === 'row') {\n * this.selectRow(event.rowIndex, event.originalEvent);\n * return true;\n * }\n * }\n * ```\n */\n onRowClick?(event: RowClickEvent): boolean | void;\n\n /**\n * Handle header click events.\n * Called when a column header is clicked. Commonly used for sorting.\n *\n * @param event - Header click event with column context\n * @returns `true` to prevent default behavior and stop propagation, `false`/`void` to allow default\n *\n * @example\n * ```ts\n * onHeaderClick(event: HeaderClickEvent): boolean | void {\n * if (event.column.sortable !== false) {\n * this.toggleSort(event.field);\n * return true;\n * }\n * }\n * ```\n */\n onHeaderClick?(event: HeaderClickEvent): boolean | void;\n\n /**\n * Handle scroll events on the grid viewport.\n * Called during scrolling. Note: This may be called frequently; debounce if needed.\n *\n * @param event - Scroll event with scroll position and viewport dimensions\n *\n * @example\n * ```ts\n * onScroll(event: ScrollEvent): void {\n * // Update sticky column positions\n * this.updateStickyPositions(event.scrollLeft);\n * }\n * ```\n */\n onScroll?(event: ScrollEvent): void;\n\n /**\n * Handle cell mousedown events.\n * Used for initiating drag operations like range selection or column resize.\n *\n * @param event - Mouse event with cell context\n * @returns `true` to indicate drag started (prevents text selection), `false`/`void` otherwise\n *\n * @example\n * ```ts\n * onCellMouseDown(event: CellMouseEvent): boolean | void {\n * if (event.rowIndex !== undefined && this.config.mode === 'range') {\n * this.startDragSelection(event.rowIndex, event.colIndex);\n * return true; // Prevent text selection\n * }\n * }\n * ```\n */\n onCellMouseDown?(event: CellMouseEvent): boolean | void;\n\n /**\n * Handle cell mousemove events during drag operations.\n * Only called when a drag is in progress (after mousedown returned true).\n *\n * @param event - Mouse event with current cell context\n * @returns `true` to continue handling the drag, `false`/`void` otherwise\n *\n * @example\n * ```ts\n * onCellMouseMove(event: CellMouseEvent): boolean | void {\n * if (this.isDragging && event.rowIndex !== undefined) {\n * this.extendSelection(event.rowIndex, event.colIndex);\n * return true;\n * }\n * }\n * ```\n */\n onCellMouseMove?(event: CellMouseEvent): boolean | void;\n\n /**\n * Handle cell mouseup events to end drag operations.\n *\n * @param event - Mouse event with final cell context\n * @returns `true` if drag was finalized, `false`/`void` otherwise\n *\n * @example\n * ```ts\n * onCellMouseUp(event: CellMouseEvent): boolean | void {\n * if (this.isDragging) {\n * this.finalizeDragSelection();\n * this.isDragging = false;\n * return true;\n * }\n * }\n * ```\n */\n onCellMouseUp?(event: CellMouseEvent): boolean | void;\n\n // Note: Context menu items are provided via handleQuery('getContextMenuItems').\n // This keeps the core decoupled from the context-menu plugin specifics.\n\n // #endregion\n\n // #region Column State Hooks\n\n /**\n * Contribute plugin-specific state for a column.\n * Called by the grid when collecting column state for serialization.\n * Plugins can add their own properties to the column state.\n *\n * @param field - The field name of the column\n * @returns Partial column state with plugin-specific properties, or undefined if no state to contribute\n *\n * @example\n * ```ts\n * getColumnState(field: string): Partial<ColumnState> | undefined {\n * const filterModel = this.filterModels.get(field);\n * if (filterModel) {\n * // Uses module augmentation to add filter property to ColumnState\n * return { filter: filterModel } as Partial<ColumnState>;\n * }\n * return undefined;\n * }\n * ```\n */\n getColumnState?(field: string): Partial<ColumnState> | undefined;\n\n /**\n * Apply plugin-specific state to a column.\n * Called by the grid when restoring column state from serialized data.\n * Plugins should restore their internal state based on the provided state.\n *\n * @param field - The field name of the column\n * @param state - The column state to apply (may contain plugin-specific properties)\n *\n * @example\n * ```ts\n * applyColumnState(field: string, state: ColumnState): void {\n * // Check for filter property added via module augmentation\n * const filter = (state as any).filter;\n * if (filter) {\n * this.filterModels.set(field, filter);\n * this.applyFilter();\n * }\n * }\n * ```\n */\n applyColumnState?(field: string, state: ColumnState): void;\n\n // #endregion\n\n // #region Scroll Boundary Hooks\n\n /**\n * Report horizontal scroll boundary offsets for this plugin.\n * Plugins that obscure part of the scroll area (e.g., pinned/sticky columns)\n * should return how much space they occupy on each side.\n * The keyboard navigation uses this to ensure focused cells are fully visible.\n *\n * @param rowEl - The row element (optional, for calculating widths from rendered cells)\n * @param focusedCell - The currently focused cell element (optional, to determine if scrolling should be skipped)\n * @returns Object with left/right pixel offsets and optional skipScroll flag, or undefined if plugin has no offsets\n *\n * @example\n * ```ts\n * getHorizontalScrollOffsets(rowEl?: HTMLElement, focusedCell?: HTMLElement): { left: number; right: number; skipScroll?: boolean } | undefined {\n * // Calculate total width of left-pinned columns\n * const leftCells = rowEl?.querySelectorAll('.sticky-left') ?? [];\n * let left = 0;\n * leftCells.forEach(el => { left += (el as HTMLElement).offsetWidth; });\n * // Skip scroll if focused cell is pinned (always visible)\n * const skipScroll = focusedCell?.classList.contains('sticky-left');\n * return { left, right: 0, skipScroll };\n * }\n * ```\n */\n getHorizontalScrollOffsets?(\n rowEl?: HTMLElement,\n focusedCell?: HTMLElement,\n ): { left: number; right: number; skipScroll?: boolean } | undefined;\n\n // #endregion\n\n // #region Shell Integration Hooks\n\n /**\n * Register a tool panel for this plugin.\n * Return undefined if plugin has no tool panel.\n * The shell will create a toolbar toggle button and render the panel content\n * when the user opens the panel.\n *\n * @returns Tool panel definition, or undefined if plugin has no panel\n *\n * @example\n * ```ts\n * getToolPanel(): ToolPanelDefinition | undefined {\n * return {\n * id: 'columns',\n * title: 'Columns',\n * icon: '☰',\n * tooltip: 'Show/hide columns',\n * order: 10,\n * render: (container) => {\n * this.renderColumnList(container);\n * return () => this.cleanup();\n * },\n * };\n * }\n * ```\n */\n getToolPanel?(): ToolPanelDefinition | undefined;\n\n /**\n * Register content for the shell header center section.\n * Return undefined if plugin has no header content.\n * Examples: search input, selection summary, status indicators.\n *\n * @returns Header content definition, or undefined if plugin has no header content\n *\n * @example\n * ```ts\n * getHeaderContent(): HeaderContentDefinition | undefined {\n * return {\n * id: 'quick-filter',\n * order: 10,\n * render: (container) => {\n * const input = document.createElement('input');\n * input.type = 'text';\n * input.placeholder = 'Search...';\n * input.addEventListener('input', this.handleInput);\n * container.appendChild(input);\n * return () => input.removeEventListener('input', this.handleInput);\n * },\n * };\n * }\n * ```\n */\n getHeaderContent?(): HeaderContentDefinition | undefined;\n\n // #endregion\n}\n","/**\n * Shared Expander Column Utilities\n *\n * Provides a fixed expander column for plugins that need expand/collapse icons\n * (MasterDetail, Tree, RowGrouping). The column is:\n * - Always first in the grid\n * - Cannot be reordered (lockPosition: true)\n * - Has no header (empty string)\n * - Has no right border (borderless styling)\n * - Narrow width (just fits the icon)\n */\n\nimport type { ColumnConfig } from '../types';\n\n/** Special field name for the expander column */\nexport const EXPANDER_COLUMN_FIELD = '__tbw_expander';\n\n/** Default width for the expander column (pixels) */\nexport const EXPANDER_COLUMN_WIDTH = 32;\n\n/**\n * Marker interface for expander column renderers.\n * Used to detect if expander column is already present.\n */\nexport interface ExpanderColumnRenderer {\n (ctx: any): HTMLElement;\n __expanderColumn?: true;\n /** Plugin name that created this expander */\n __expanderPlugin?: string;\n}\n\n/**\n * Check if a column is an expander column.\n */\nexport function isExpanderColumn(column: ColumnConfig): boolean {\n return column.field === EXPANDER_COLUMN_FIELD;\n}\n\n/**\n * Check if a column is a utility column (excluded from selection, clipboard, etc.).\n * Utility columns are non-data columns like expander columns.\n */\nexport function isUtilityColumn(column: ColumnConfig): boolean {\n return column.meta?.utility === true;\n}\n\n/**\n * Find an existing expander column in the column array.\n */\nexport function findExpanderColumn(columns: readonly ColumnConfig[]): ColumnConfig | undefined {\n return columns.find(isExpanderColumn);\n}\n\n/**\n * Create the base expander column config.\n * Plugins should add their own renderer to customize the expand icon behavior.\n *\n * @param pluginName - Name of the plugin creating the expander (for debugging)\n * @returns Base column config for the expander column\n */\nexport function createExpanderColumnConfig(pluginName: string): ColumnConfig {\n return {\n field: EXPANDER_COLUMN_FIELD as any,\n header: '', // No header text - visually merges with next column\n width: EXPANDER_COLUMN_WIDTH,\n resizable: false,\n sortable: false,\n filterable: false, // No filter button for expander column\n meta: {\n lockPosition: true,\n suppressMovable: true,\n expanderColumn: true,\n expanderPlugin: pluginName,\n utility: true, // Marks this as a utility column (excluded from selection, clipboard, etc.)\n },\n };\n}\n\n/**\n * Create a container element for expand/collapse toggle icons.\n * Used by plugins to wrap their expand icons with consistent styling.\n *\n * @param expanded - Whether the item is currently expanded\n * @param pluginClass - CSS class prefix for the plugin (e.g., 'master-detail', 'tree')\n * @returns Container span element\n */\nexport function createExpanderContainer(expanded: boolean, pluginClass: string): HTMLSpanElement {\n const container = document.createElement('span');\n container.className = `${pluginClass}-expander expander-cell`;\n if (expanded) {\n container.classList.add('expanded');\n }\n return container;\n}\n\n/**\n * CSS styles for the expander column.\n * Plugins should include this in their styles to ensure consistent appearance.\n */\nexport const EXPANDER_COLUMN_STYLES = `\n/* Expander column data cells - always first, borderless right edge */\n.cell[data-field=\"${EXPANDER_COLUMN_FIELD}\"] {\n border-right: none !important;\n padding: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n/* Expander column header - completely hidden, no content, no border, no width contribution */\n.header-row .cell[data-field=\"${EXPANDER_COLUMN_FIELD}\"] {\n visibility: hidden;\n border: none !important;\n padding: 0;\n overflow: hidden;\n}\n\n/* The column after the expander should visually extend into the expander header space */\n.header-row .cell[data-field=\"${EXPANDER_COLUMN_FIELD}\"] + .cell {\n /* Pull left to cover the hidden expander header */\n margin-left: -${EXPANDER_COLUMN_WIDTH}px;\n padding-left: calc(var(--tbw-cell-padding, 8px) + ${EXPANDER_COLUMN_WIDTH}px);\n}\n\n/* Expander cell contents */\n.expander-cell {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 100%;\n height: 100%;\n cursor: pointer;\n}\n`;\n","/**\n * Filtering Plugin (Class-based)\n *\n * Provides comprehensive filtering functionality for tbw-grid.\n * Supports text, number, date, set, and boolean filters with caching.\n * Includes UI with filter buttons in headers and dropdown filter panels.\n */\n\nimport { computeVirtualWindow, shouldBypassVirtualization } from '../../core/internal/virtualization';\nimport { BaseGridPlugin, type GridElement, type PluginManifest, type PluginQuery } from '../../core/plugin/base-plugin';\nimport { isUtilityColumn } from '../../core/plugin/expander-column';\nimport type { ColumnConfig, ColumnState } from '../../core/types';\nimport type { ContextMenuParams, HeaderContextMenuItem } from '../context-menu/types';\nimport { computeFilterCacheKey, filterRows, getUniqueValues, getUniqueValuesBatch } from './filter-model';\nimport styles from './filtering.css?inline';\nimport filterPanelStyles from './FilteringPlugin.css?inline';\nimport type { FilterChangeDetail, FilterConfig, FilterModel, FilterPanelParams } from './types';\n\n/**\n * Filtering Plugin for tbw-grid\n *\n * Adds column header filters with text search, dropdown options, and custom filter panels.\n * Supports both **local filtering** for small datasets and **async handlers** for server-side\n * filtering on large datasets.\n *\n * ## Installation\n *\n * ```ts\n * import { FilteringPlugin } from '@toolbox-web/grid/plugins/filtering';\n * ```\n *\n * ## Configuration Options\n *\n * | Option | Type | Default | Description |\n * |--------|------|---------|-------------|\n * | `debounceMs` | `number` | `300` | Debounce delay for filter input |\n * | `caseSensitive` | `boolean` | `false` | Case-sensitive string matching |\n * | `trimInput` | `boolean` | `true` | Trim whitespace from filter input |\n * | `useWorker` | `boolean` | `true` | Use Web Worker for datasets >1000 rows |\n * | `filterPanelRenderer` | `FilterPanelRenderer` | - | Custom filter panel renderer |\n * | `valuesHandler` | `FilterValuesHandler` | - | Async handler to fetch unique filter values |\n * | `filterHandler` | `FilterHandler<TRow>` | - | Async handler to apply filters remotely |\n *\n * ## Column Configuration\n *\n * | Property | Type | Description |\n * |----------|------|-------------|\n * | `filterable` | `boolean` | Enable filtering for this column |\n * | `filterType` | `'text' \\| 'select' \\| 'number' \\| 'date'` | Filter UI type |\n * | `filterOptions` | `unknown[]` | Predefined options for select filters |\n *\n * ## Programmatic API\n *\n * | Method | Signature | Description |\n * |--------|-----------|-------------|\n * | `setFilter` | `(field, value) => void` | Set filter value for a column |\n * | `getFilters` | `() => FilterModel[]` | Get all current filters |\n * | `clearFilters` | `() => void` | Clear all filters |\n * | `clearFilter` | `(field) => void` | Clear filter for a specific column |\n *\n * ## CSS Custom Properties\n *\n * | Property | Default | Description |\n * |----------|---------|-------------|\n * | `--tbw-filter-panel-bg` | `var(--tbw-color-panel-bg)` | Panel background |\n * | `--tbw-filter-panel-fg` | `var(--tbw-color-fg)` | Panel text color |\n * | `--tbw-filter-panel-border` | `var(--tbw-color-border)` | Panel border |\n * | `--tbw-filter-active-color` | `var(--tbw-color-accent)` | Active filter indicator |\n * | `--tbw-filter-input-bg` | `var(--tbw-color-bg)` | Input background |\n * | `--tbw-filter-input-focus` | `var(--tbw-color-accent)` | Input focus border |\n *\n * @example Basic Usage with Filterable Columns\n * ```ts\n * import '@toolbox-web/grid';\n * import { FilteringPlugin } from '@toolbox-web/grid/plugins/filtering';\n *\n * const grid = document.querySelector('tbw-grid');\n * grid.gridConfig = {\n * columns: [\n * { field: 'name', header: 'Name', filterable: true },\n * { field: 'status', header: 'Status', filterable: true, filterType: 'select' },\n * { field: 'email', header: 'Email', filterable: true },\n * ],\n * plugins: [new FilteringPlugin({ debounceMs: 300 })],\n * };\n * grid.rows = data;\n * ```\n *\n * @example Column Formatters in Filter Panel\n * When a column defines a `format` function, the built-in set filter panel\n * displays formatted labels instead of raw values. Search within the panel\n * also matches against the formatted text.\n * ```ts\n * grid.gridConfig = {\n * columns: [\n * {\n * field: 'price',\n * filterable: true,\n * format: (value) => `$${Number(value).toFixed(2)}`,\n * // Filter checkboxes show \"$9.99\" instead of \"9.99\"\n * },\n * ],\n * plugins: [new FilteringPlugin()],\n * };\n * ```\n *\n * @example Server-Side Filtering with Async Handlers\n * ```ts\n * new FilteringPlugin({\n * // Fetch unique values from server for filter dropdown\n * valuesHandler: async (field, column) => {\n * const response = await fetch(`/api/distinct-values?field=${field}`);\n * return response.json();\n * },\n * // Apply filters on the server\n * filterHandler: async (filters, currentRows) => {\n * const response = await fetch('/api/data', {\n * method: 'POST',\n * body: JSON.stringify({ filters }),\n * });\n * return response.json();\n * },\n * });\n * ```\n *\n * @see {@link FilterConfig} for all configuration options\n * @see {@link FilterModel} for filter data structure\n * @see {@link FilterPanelParams} for custom panel renderer parameters\n *\n * @internal Extends BaseGridPlugin\n */\nexport class FilteringPlugin extends BaseGridPlugin<FilterConfig> {\n /**\n * Plugin manifest - declares events emitted by this plugin.\n * @internal\n */\n static override readonly manifest: PluginManifest = {\n events: [\n {\n type: 'filter-applied',\n description: 'Emitted when filter criteria change. Subscribers can react to row visibility changes.',\n },\n ],\n queries: [\n {\n type: 'getContextMenuItems',\n description: 'Contributes filter-related items to the header context menu',\n },\n ],\n };\n\n /** @internal */\n readonly name = 'filtering';\n /** @internal */\n override readonly styles = styles;\n\n /** @internal */\n protected override get defaultConfig(): Partial<FilterConfig> {\n return {\n debounceMs: 300,\n caseSensitive: false,\n trimInput: true,\n useWorker: true,\n };\n }\n\n // #region Helpers\n\n /**\n * Check if filtering is enabled at the grid level.\n * Grid-wide `filterable: false` disables filtering for all columns.\n */\n private isFilteringEnabled(): boolean {\n return this.grid.effectiveConfig?.filterable !== false;\n }\n\n /**\n * Check if a specific column is filterable, respecting both grid-level and column-level settings.\n */\n private isColumnFilterable(col: { filterable?: boolean; field?: string }): boolean {\n if (!this.isFilteringEnabled()) return false;\n return col.filterable !== false;\n }\n\n /**\n * Build a map of field → filterValue extractor for columns that have one.\n * Used to pass array-aware value extraction to the pure filter functions.\n */\n private getFilterValues():\n | Map<string, (value: unknown, row: Record<string, unknown>) => unknown | unknown[]>\n | undefined {\n const columns = this.grid.effectiveConfig?.columns;\n if (!columns) return undefined;\n\n let map: Map<string, (value: unknown, row: Record<string, unknown>) => unknown | unknown[]> | undefined;\n for (const col of columns) {\n if (col.field && col.filterValue) {\n if (!map) map = new Map();\n map.set(col.field, col.filterValue);\n }\n }\n return map;\n }\n\n // #endregion\n\n // #region Internal State\n private filters: Map<string, FilterModel> = new Map();\n private cachedResult: unknown[] | null = null;\n private cacheKey: string | null = null;\n /** Spot-check of input rows for cache invalidation when upstream plugins (e.g. sort) change row order */\n private cachedInputSpot: { len: number; first: unknown; mid: unknown; last: unknown } | null = null;\n private openPanelField: string | null = null;\n private panelElement: HTMLElement | null = null;\n private panelAnchorElement: HTMLElement | null = null; // For CSS anchor positioning cleanup\n private searchText: Map<string, string> = new Map();\n private excludedValues: Map<string, Set<unknown>> = new Map();\n private panelAbortController: AbortController | null = null; // For panel-scoped listeners\n private globalStylesInjected = false;\n\n // Virtualization constants for filter value list\n private static readonly DEFAULT_LIST_ITEM_HEIGHT = 28;\n private static readonly LIST_OVERSCAN = 3;\n private static readonly LIST_BYPASS_THRESHOLD = 50; // Don't virtualize if < 50 items\n\n /**\n * Get the item height from CSS variable or fallback to default.\n * Reads --tbw-filter-item-height from the panel element.\n */\n private getListItemHeight(): number {\n if (this.panelElement) {\n const cssValue = getComputedStyle(this.panelElement).getPropertyValue('--tbw-filter-item-height');\n if (cssValue && cssValue.trim()) {\n const parsed = parseFloat(cssValue);\n if (!isNaN(parsed) && parsed > 0) {\n return parsed;\n }\n }\n }\n return FilteringPlugin.DEFAULT_LIST_ITEM_HEIGHT;\n }\n\n /**\n * Compute the inclusion (selected) map from the current filters and excluded values.\n * For set filters this is: uniqueValues \\ excludedValues.\n * Only includes entries for fields that have a set filter.\n * Uses a single-pass batch extraction to avoid iterating sourceRows per field.\n */\n private computeSelected(): Record<string, unknown[]> {\n // Collect the fields that need unique values\n const setFields: {\n field: string;\n filterValue?: (value: unknown, row: Record<string, unknown>) => unknown | unknown[];\n }[] = [];\n for (const [field, filter] of this.filters) {\n if (filter.type !== 'set' || filter.operator !== 'notIn') continue;\n const col = this.grid.effectiveConfig?.columns?.find((c) => c.field === field);\n setFields.push({ field, filterValue: col?.filterValue });\n }\n if (setFields.length === 0) return {};\n\n // Single pass through sourceRows for all fields\n const uniqueMap = getUniqueValuesBatch(this.sourceRows as Record<string, unknown>[], setFields);\n\n const selected: Record<string, unknown[]> = {};\n for (const { field } of setFields) {\n const excluded = this.excludedValues.get(field);\n const unique = uniqueMap.get(field) ?? [];\n selected[field] = excluded ? unique.filter((v) => !excluded.has(v)) : unique;\n }\n return selected;\n }\n\n /**\n * Sync excludedValues map from a filter model (for set filters).\n */\n private syncExcludedValues(field: string, filter: FilterModel | null): void {\n if (!filter) {\n this.excludedValues.delete(field);\n } else if (filter.type === 'set' && filter.operator === 'notIn' && Array.isArray(filter.value)) {\n this.excludedValues.set(field, new Set(filter.value));\n } else if (filter.type === 'set') {\n // Other set operators may have different semantics; clear for safety\n this.excludedValues.delete(field);\n }\n }\n // #endregion\n\n // #region Lifecycle\n\n /** @internal */\n override attach(grid: GridElement): void {\n super.attach(grid);\n this.injectGlobalStyles();\n }\n\n /** @internal */\n override detach(): void {\n this.filters.clear();\n this.cachedResult = null;\n this.cacheKey = null;\n this.cachedInputSpot = null;\n this.openPanelField = null;\n if (this.panelElement) {\n this.panelElement.remove();\n this.panelElement = null;\n }\n this.searchText.clear();\n this.excludedValues.clear();\n // Abort panel-scoped listeners (document click handler, etc.)\n this.panelAbortController?.abort();\n this.panelAbortController = null;\n }\n // #endregion\n\n // #region Query Handlers\n\n /**\n * Handle inter-plugin queries.\n * Contributes filter-related items to the header context menu.\n * @internal\n */\n override handleQuery(query: PluginQuery): unknown {\n if (query.type === 'getContextMenuItems') {\n const params = query.context as ContextMenuParams;\n if (!params.isHeader) return undefined;\n\n const column = params.column as ColumnConfig;\n if (!column?.field) return undefined;\n\n // Only contribute items if filtering is enabled for this column\n if (!this.isFilteringEnabled()) return undefined;\n if (!this.isColumnFilterable(column)) return undefined;\n\n const items: HeaderContextMenuItem[] = [];\n const fieldFiltered = this.isFieldFiltered(column.field);\n const hasAnyFilter = this.filters.size > 0;\n\n if (fieldFiltered) {\n items.push({\n id: 'filtering/clear-column-filter',\n label: `Clear Filter`,\n icon: '✕',\n order: 20,\n action: () => this.clearFieldFilter(column.field),\n });\n }\n\n if (hasAnyFilter) {\n items.push({\n id: 'filtering/clear-all-filters',\n label: 'Clear All Filters',\n icon: '✕',\n order: 21,\n disabled: !hasAnyFilter,\n action: () => this.clearAllFilters(),\n });\n }\n\n return items.length > 0 ? items : undefined;\n }\n return undefined;\n }\n // #endregion\n\n // #region Hooks\n\n /** @internal */\n override processRows(rows: readonly unknown[]): unknown[] {\n const filterList = [...this.filters.values()];\n if (!filterList.length) return [...rows];\n\n // If using async filterHandler, processRows becomes a passthrough\n // Actual filtering happens in applyFiltersAsync and rows are set directly on grid\n if (this.config.filterHandler) {\n // Return cached result if available (set by async handler)\n if (this.cachedResult) return this.cachedResult;\n // Otherwise return rows as-is (filtering happens async)\n return [...rows];\n }\n\n // Check cache — also verify input rows haven't changed (e.g. due to sort)\n const newCacheKey = computeFilterCacheKey(filterList);\n const inputSpot = {\n len: rows.length,\n first: rows[0],\n mid: rows[Math.floor(rows.length / 2)],\n last: rows[rows.length - 1],\n };\n const inputUnchanged =\n this.cachedInputSpot != null &&\n inputSpot.len === this.cachedInputSpot.len &&\n inputSpot.first === this.cachedInputSpot.first &&\n inputSpot.mid === this.cachedInputSpot.mid &&\n inputSpot.last === this.cachedInputSpot.last;\n\n if (this.cacheKey === newCacheKey && this.cachedResult && inputUnchanged) {\n return this.cachedResult;\n }\n\n // Filter rows synchronously (worker support can be added later)\n const result = filterRows(\n [...rows] as Record<string, unknown>[],\n filterList,\n this.config.caseSensitive,\n this.getFilterValues(),\n );\n\n // Update cache\n this.cachedResult = result;\n this.cacheKey = newCacheKey;\n this.cachedInputSpot = inputSpot;\n\n return result;\n }\n\n /** @internal */\n override afterRender(): void {\n const gridEl = this.gridElement;\n if (!gridEl) return;\n\n // Find all header cells (using part attribute, not class)\n const headerCells = gridEl.querySelectorAll('[part~=\"header-cell\"]');\n headerCells.forEach((cell) => {\n const colIndex = cell.getAttribute('data-col');\n if (colIndex === null) return;\n\n // Use visibleColumns since data-col is the index within _visibleColumns\n const col = this.visibleColumns[parseInt(colIndex, 10)] as ColumnConfig;\n if (!col || !this.isColumnFilterable(col)) return;\n\n // Skip utility columns (expander, selection checkbox, etc.)\n if (isUtilityColumn(col)) return;\n\n const field = col.field;\n if (!field) return;\n\n const hasFilter = this.filters.has(field);\n\n // Check if button already exists\n let filterBtn = cell.querySelector('.tbw-filter-btn') as HTMLElement | null;\n\n if (filterBtn) {\n // Update active state and icon of existing button\n const wasActive = filterBtn.classList.contains('active');\n filterBtn.classList.toggle('active', hasFilter);\n (cell as HTMLElement).classList.toggle('filtered', hasFilter);\n // Update icon if active state changed\n if (wasActive !== hasFilter) {\n const iconName = hasFilter ? 'filterActive' : 'filter';\n this.setIcon(filterBtn, this.resolveIcon(iconName));\n }\n return;\n }\n\n // Create filter button\n filterBtn = document.createElement('button');\n filterBtn.className = 'tbw-filter-btn';\n filterBtn.setAttribute('aria-label', `Filter ${col.header ?? field}`);\n // Use grid icons configuration\n const iconName = hasFilter ? 'filterActive' : 'filter';\n this.setIcon(filterBtn, this.resolveIcon(iconName));\n\n // Mark button as active if filter exists\n if (hasFilter) {\n filterBtn.classList.add('active');\n (cell as HTMLElement).classList.add('filtered');\n }\n\n filterBtn.addEventListener('click', (e) => {\n e.stopPropagation();\n this.toggleFilterPanel(field, col, filterBtn!);\n });\n\n // Insert before resize handle to maintain order: [label, sort-indicator, filter-btn, resize-handle]\n const resizeHandle = cell.querySelector('.resize-handle');\n if (resizeHandle) {\n cell.insertBefore(filterBtn, resizeHandle);\n } else {\n cell.appendChild(filterBtn);\n }\n });\n }\n // #endregion\n\n // #region Public API\n\n /**\n * Set a filter on a specific field.\n * Pass null to remove the filter.\n */\n setFilter(field: string, filter: Omit<FilterModel, 'field'> | null): void {\n if (filter === null) {\n this.filters.delete(field);\n this.syncExcludedValues(field, null);\n } else {\n const fullFilter = { ...filter, field };\n this.filters.set(field, fullFilter);\n this.syncExcludedValues(field, fullFilter);\n }\n // Invalidate cache\n this.cachedResult = null;\n this.cacheKey = null;\n this.cachedInputSpot = null;\n\n this.emit<FilterChangeDetail>('filter-change', {\n filters: [...this.filters.values()],\n filteredRowCount: 0, // Will be accurate after processRows\n selected: this.computeSelected(),\n });\n // Notify other plugins via Event Bus\n this.emitPluginEvent('filter-applied', { filters: [...this.filters.values()] });\n this.requestRender();\n }\n\n /**\n * Get the current filter for a field.\n */\n getFilter(field: string): FilterModel | undefined {\n return this.filters.get(field);\n }\n\n /**\n * Get all active filters.\n */\n getFilters(): FilterModel[] {\n return [...this.filters.values()];\n }\n\n /**\n * Alias for getFilters() to match functional API naming.\n */\n getFilterModel(): FilterModel[] {\n return this.getFilters();\n }\n\n /**\n * Set filters from an array (replaces all existing filters).\n */\n setFilterModel(filters: FilterModel[]): void {\n this.filters.clear();\n this.excludedValues.clear();\n for (const filter of filters) {\n this.filters.set(filter.field, filter);\n this.syncExcludedValues(filter.field, filter);\n }\n this.cachedResult = null;\n this.cacheKey = null;\n this.cachedInputSpot = null;\n\n this.emit<FilterChangeDetail>('filter-change', {\n filters: [...this.filters.values()],\n filteredRowCount: 0,\n selected: this.computeSelected(),\n });\n // Notify other plugins via Event Bus\n this.emitPluginEvent('filter-applied', { filters: [...this.filters.values()] });\n this.requestRender();\n }\n\n /**\n * Clear all filters.\n */\n clearAllFilters(): void {\n this.filters.clear();\n this.excludedValues.clear();\n this.searchText.clear();\n\n this.applyFiltersInternal();\n }\n\n /**\n * Clear filter for a specific field.\n */\n clearFieldFilter(field: string): void {\n this.filters.delete(field);\n this.excludedValues.delete(field);\n this.searchText.delete(field);\n\n this.applyFiltersInternal();\n }\n\n /**\n * Check if a field has an active filter.\n */\n isFieldFiltered(field: string): boolean {\n return this.filters.has(field);\n }\n\n /**\n * Get the count of filtered rows (from cache).\n */\n getFilteredRowCount(): number {\n return this.cachedResult?.length ?? this.rows.length;\n }\n\n /**\n * Get all active filters (alias for getFilters).\n */\n getActiveFilters(): FilterModel[] {\n return this.getFilters();\n }\n\n /**\n * Get unique values for a field (for set filter dropdowns).\n * Uses sourceRows to include all values regardless of current filter.\n * When a column has `filterValue`, individual extracted values are returned.\n */\n getUniqueValues(field: string): unknown[] {\n const col = this.grid.effectiveConfig?.columns?.find((c) => c.field === field);\n const getter = col?.filterValue;\n return getUniqueValues(this.sourceRows as Record<string, unknown>[], field, getter);\n }\n // #endregion\n\n // #region Private Methods\n\n /**\n * Copy CSS classes and data attributes from grid to filter panel.\n * This ensures theme classes (e.g., .eds-theme) cascade to the panel.\n */\n private copyGridThemeContext(panel: HTMLElement): void {\n const gridEl = this.gridElement;\n if (!gridEl) return;\n\n // Copy all CSS classes from grid to panel (except internal ones)\n for (const className of gridEl.classList) {\n // Skip internal classes that shouldn't be copied\n if (className.startsWith('tbw-') || className === 'selecting') continue;\n panel.classList.add(className);\n }\n\n // Copy data-theme attribute if present\n const theme = gridEl.dataset.theme;\n if (theme) {\n panel.dataset.theme = theme;\n }\n }\n\n /**\n * Inject global styles for filter panel (rendered in document.body)\n */\n private injectGlobalStyles(): void {\n if (this.globalStylesInjected) return;\n if (document.getElementById('tbw-filter-panel-styles')) {\n this.globalStylesInjected = true;\n return;\n }\n // Only inject if we have valid CSS text (Vite's ?inline import)\n // When importing from source without Vite, the import is a module object, not a string\n if (typeof filterPanelStyles !== 'string' || !filterPanelStyles) {\n this.globalStylesInjected = true;\n return;\n }\n const style = document.createElement('style');\n style.id = 'tbw-filter-panel-styles';\n style.textContent = filterPanelStyles;\n document.head.appendChild(style);\n this.globalStylesInjected = true;\n }\n\n /**\n * Toggle the filter panel for a field\n */\n private toggleFilterPanel(field: string, column: ColumnConfig, buttonEl: HTMLElement): void {\n // Close if already open\n if (this.openPanelField === field) {\n this.closeFilterPanel();\n return;\n }\n\n // Close any existing panel\n this.closeFilterPanel();\n\n // Create panel\n const panel = document.createElement('div');\n panel.className = 'tbw-filter-panel';\n // Copy theme classes from grid to panel for proper theming\n this.copyGridThemeContext(panel);\n // Add animation class if animations are enabled\n if (this.isAnimationEnabled) {\n panel.classList.add('tbw-filter-panel-animated');\n }\n this.panelElement = panel;\n this.openPanelField = field;\n\n // If using async valuesHandler, show loading state and fetch values\n if (this.config.valuesHandler) {\n panel.innerHTML = '<div class=\"tbw-filter-loading\">Loading...</div>';\n document.body.appendChild(panel);\n this.positionPanel(panel, buttonEl);\n this.setupPanelCloseHandler(panel, buttonEl);\n\n this.config.valuesHandler(field, column).then((values) => {\n // Check if panel is still open for this field\n if (this.openPanelField !== field || !this.panelElement) return;\n panel.innerHTML = '';\n this.renderPanelContent(field, column, panel, values);\n });\n return;\n }\n\n // Sync path: get unique values from local rows\n const uniqueValues = getUniqueValues(this.sourceRows as Record<string, unknown>[], field, column.filterValue);\n\n // Position and append to body BEFORE rendering content\n // so getListItemHeight() can read CSS variables from computed styles\n document.body.appendChild(panel);\n this.positionPanel(panel, buttonEl);\n\n this.renderPanelContent(field, column, panel, uniqueValues);\n this.setupPanelCloseHandler(panel, buttonEl);\n }\n\n /**\n * Render filter panel content with given values\n */\n private renderPanelContent(field: string, column: ColumnConfig, panel: HTMLElement, uniqueValues: unknown[]): void {\n // Get current excluded values or initialize empty\n let excludedSet = this.excludedValues.get(field);\n if (!excludedSet) {\n excludedSet = new Set();\n this.excludedValues.set(field, excludedSet);\n }\n\n // Get current search text\n const currentSearchText = this.searchText.get(field) ?? '';\n\n // Create panel params for custom renderer\n const params: FilterPanelParams = {\n field,\n column,\n uniqueValues,\n excludedValues: excludedSet,\n searchText: currentSearchText,\n applySetFilter: (excluded: unknown[]) => {\n this.applySetFilter(field, excluded);\n this.closeFilterPanel();\n },\n applyTextFilter: (operator, value, valueTo) => {\n this.applyTextFilter(field, operator, value, valueTo);\n this.closeFilterPanel();\n },\n clearFilter: () => {\n this.clearFieldFilter(field);\n this.closeFilterPanel();\n },\n closePanel: () => this.closeFilterPanel(),\n };\n\n // Use custom renderer or default\n // Custom renderer can return undefined to fall back to default panel for specific columns\n // Resolution order: plugin config → typeDefaults → built-in\n let usedCustomRenderer = false;\n\n // 1. Check plugin-level filterPanelRenderer\n if (this.config.filterPanelRenderer) {\n this.config.filterPanelRenderer(panel, params);\n // If renderer added content to panel, it handled rendering\n usedCustomRenderer = panel.children.length > 0;\n }\n\n // 2. Check typeDefaults for this column's type\n if (!usedCustomRenderer && column.type) {\n const typeDefault = this.grid.effectiveConfig.typeDefaults?.[column.type];\n if (typeDefault?.filterPanelRenderer) {\n typeDefault.filterPanelRenderer(panel, params);\n usedCustomRenderer = panel.children.length > 0;\n }\n }\n\n // 3. Fall back to built-in type-specific panel renderers\n if (!usedCustomRenderer) {\n const columnType = column.type;\n if (columnType === 'number') {\n this.renderNumberFilterPanel(panel, params, uniqueValues);\n } else if (columnType === 'date') {\n this.renderDateFilterPanel(panel, params, uniqueValues);\n } else {\n this.renderDefaultFilterPanel(panel, params, uniqueValues, excludedSet);\n }\n }\n }\n\n /**\n * Setup click-outside handler to close the panel\n */\n private setupPanelCloseHandler(panel: HTMLElement, buttonEl: HTMLElement): void {\n // Create abort controller for panel-scoped listeners\n // This allows cleanup when panel closes OR when grid disconnects\n this.panelAbortController = new AbortController();\n\n // Add global click handler to close on outside click\n // Defer to next tick to avoid immediate close from the click that opened the panel\n setTimeout(() => {\n document.addEventListener(\n 'click',\n (e: MouseEvent) => {\n if (!panel.contains(e.target as Node) && e.target !== buttonEl) {\n this.closeFilterPanel();\n }\n },\n { signal: this.panelAbortController?.signal },\n );\n }, 0);\n }\n\n /**\n * Close the filter panel\n */\n private closeFilterPanel(): void {\n const panel = this.panelElement;\n if (panel) {\n panel.remove();\n this.panelElement = null;\n }\n // Clean up anchor name from header cell\n if (this.panelAnchorElement) {\n (this.panelAnchorElement.style as any).anchorName = '';\n this.panelAnchorElement = null;\n }\n this.openPanelField = null;\n // Abort panel-scoped listeners (document click handler)\n this.panelAbortController?.abort();\n this.panelAbortController = null;\n }\n\n /** Cache for CSS anchor positioning support check */\n private static supportsAnchorPositioning: boolean | null = null;\n\n /**\n * Check if browser supports CSS Anchor Positioning\n */\n private static checkAnchorPositioningSupport(): boolean {\n if (FilteringPlugin.supportsAnchorPositioning === null) {\n FilteringPlugin.supportsAnchorPositioning = CSS.supports('anchor-name', '--test');\n }\n return FilteringPlugin.supportsAnchorPositioning;\n }\n\n /**\n * Position the panel below the header cell\n * Uses CSS Anchor Positioning if supported, falls back to JS positioning\n */\n private positionPanel(panel: HTMLElement, buttonEl: HTMLElement): void {\n // Find the parent header cell\n const headerCell = buttonEl.closest('.cell') as HTMLElement | null;\n const anchorEl = headerCell ?? buttonEl;\n\n // Set anchor name on the header cell for CSS anchor positioning\n (anchorEl.style as any).anchorName = '--tbw-filter-anchor';\n this.panelAnchorElement = anchorEl; // Store for cleanup\n\n // If CSS Anchor Positioning is supported, CSS handles positioning\n // but we need to detect if it flipped above to adjust animation\n if (FilteringPlugin.checkAnchorPositioningSupport()) {\n // Check position after CSS anchor positioning takes effect\n requestAnimationFrame(() => {\n const panelRect = panel.getBoundingClientRect();\n const anchorRect = anchorEl.getBoundingClientRect();\n // If panel top is above anchor top, it flipped to above\n if (panelRect.top < anchorRect.top) {\n panel.classList.add('tbw-filter-panel-above');\n }\n });\n return;\n }\n\n // Fallback: JS-based positioning for older browsers\n const rect = anchorEl.getBoundingClientRect();\n\n panel.style.position = 'fixed';\n panel.style.top = `${rect.bottom + 4}px`;\n panel.style.left = `${rect.left}px`;\n\n // Adjust if overflows viewport edges\n requestAnimationFrame(() => {\n const panelRect = panel.getBoundingClientRect();\n\n // Check horizontal overflow - align right edge to header cell right edge\n if (panelRect.right > window.innerWidth - 8) {\n panel.style.left = `${rect.right - panelRect.width}px`;\n }\n\n // Check vertical overflow - flip to above header cell\n if (panelRect.bottom > window.innerHeight - 8) {\n panel.style.top = `${rect.top - panelRect.height - 4}px`;\n panel.classList.add('tbw-filter-panel-above');\n }\n });\n }\n\n /**\n * Render the default filter panel content\n */\n private renderDefaultFilterPanel(\n panel: HTMLElement,\n params: FilterPanelParams,\n uniqueValues: unknown[],\n excludedValues: Set<unknown>,\n ): void {\n const { field, column } = params;\n // Get item height from CSS variable or use default\n const itemHeight = this.getListItemHeight();\n\n // Helper: format a value using the column's format function (for ID-to-name translation, etc.)\n // When filterValue is set, unique values are already extracted primitives — skip format.\n const formatValue = (value: unknown): string => {\n if (value == null) return '(Blank)';\n if (column.format && !column.filterValue) {\n const formatted = column.format(value, undefined as never);\n if (formatted) return formatted;\n }\n return String(value);\n };\n\n // Sort unique values by formatted display name\n uniqueValues = uniqueValues.slice().sort((a, b) => formatValue(a).localeCompare(formatValue(b)));\n\n // Search input\n const searchContainer = document.createElement('div');\n searchContainer.className = 'tbw-filter-search';\n\n const searchInput = document.createElement('input');\n searchInput.type = 'text';\n searchInput.placeholder = 'Search...';\n searchInput.className = 'tbw-filter-search-input';\n searchInput.value = this.searchText.get(field) ?? '';\n searchContainer.appendChild(searchInput);\n panel.appendChild(searchContainer);\n\n // Select All tristate checkbox\n const actionsRow = document.createElement('div');\n actionsRow.className = 'tbw-filter-actions';\n\n const selectAllLabel = document.createElement('label');\n selectAllLabel.className = 'tbw-filter-value-item';\n selectAllLabel.style.padding = '0';\n selectAllLabel.style.margin = '0';\n\n const selectAllCheckbox = document.createElement('input');\n selectAllCheckbox.type = 'checkbox';\n selectAllCheckbox.className = 'tbw-filter-checkbox';\n\n const selectAllText = document.createElement('span');\n selectAllText.textContent = 'Select All';\n\n selectAllLabel.appendChild(selectAllCheckbox);\n selectAllLabel.appendChild(selectAllText);\n actionsRow.appendChild(selectAllLabel);\n\n // Update tristate checkbox based on checkState\n const updateSelectAllState = () => {\n const values = [...checkState.values()];\n const allChecked = values.every((v) => v);\n const noneChecked = values.every((v) => !v);\n\n selectAllCheckbox.checked = allChecked;\n selectAllCheckbox.indeterminate = !allChecked && !noneChecked;\n };\n\n // Toggle all on click\n selectAllCheckbox.addEventListener('change', () => {\n const newState = selectAllCheckbox.checked;\n for (const key of checkState.keys()) {\n checkState.set(key, newState);\n }\n updateSelectAllState();\n renderVisibleItems();\n });\n\n panel.appendChild(actionsRow);\n\n // Values container with virtualization support\n const valuesContainer = document.createElement('div');\n valuesContainer.className = 'tbw-filter-values';\n\n // Spacer for virtual height\n const spacer = document.createElement('div');\n spacer.className = 'tbw-filter-values-spacer';\n valuesContainer.appendChild(spacer);\n\n // Content container positioned absolutely\n const contentContainer = document.createElement('div');\n contentContainer.className = 'tbw-filter-values-content';\n valuesContainer.appendChild(contentContainer);\n\n // Track current check state for values (persists across virtualizations)\n const checkState = new Map<string, boolean>();\n uniqueValues.forEach((value) => {\n const key = value == null ? '__null__' : String(value);\n checkState.set(key, !excludedValues.has(value));\n });\n\n // Initialize select all state\n updateSelectAllState();\n\n // Filtered values cache\n let filteredValues: unknown[] = [];\n\n // Create a single checkbox item element\n const createItem = (value: unknown, index: number): HTMLElement => {\n const displayValue = formatValue(value);\n const key = value == null ? '__null__' : String(value);\n\n const item = document.createElement('label');\n item.className = 'tbw-filter-value-item';\n item.style.position = 'absolute';\n item.style.top = `calc(var(--tbw-filter-item-height, 28px) * ${index})`;\n item.style.left = '0';\n item.style.right = '0';\n item.style.boxSizing = 'border-box';\n\n const checkbox = document.createElement('input');\n checkbox.type = 'checkbox';\n checkbox.className = 'tbw-filter-checkbox';\n checkbox.checked = checkState.get(key) ?? true;\n checkbox.dataset.value = key;\n\n // Sync check state on change and update tristate checkbox\n checkbox.addEventListener('change', () => {\n checkState.set(key, checkbox.checked);\n updateSelectAllState();\n });\n\n const label = document.createElement('span');\n label.textContent = displayValue;\n\n item.appendChild(checkbox);\n item.appendChild(label);\n return item;\n };\n\n // Render visible items using virtualization\n const renderVisibleItems = () => {\n const totalItems = filteredValues.length;\n const viewportHeight = valuesContainer.clientHeight;\n const scrollTop = valuesContainer.scrollTop;\n\n // Set total height for scrollbar\n spacer.style.height = `${totalItems * itemHeight}px`;\n\n // Bypass virtualization for small lists\n if (shouldBypassVirtualization(totalItems, FilteringPlugin.LIST_BYPASS_THRESHOLD / 3)) {\n contentContainer.innerHTML = '';\n contentContainer.style.transform = 'translateY(0px)';\n filteredValues.forEach((value, idx) => {\n contentContainer.appendChild(createItem(value, idx));\n });\n return;\n }\n\n // Use computeVirtualWindow for real-scroll virtualization\n const window = computeVirtualWindow({\n totalRows: totalItems,\n viewportHeight,\n scrollTop,\n rowHeight: itemHeight,\n overscan: FilteringPlugin.LIST_OVERSCAN,\n });\n\n // Position content container\n contentContainer.style.transform = `translateY(${window.offsetY}px)`;\n\n // Clear and render visible items\n contentContainer.innerHTML = '';\n for (let i = window.start; i < window.end; i++) {\n contentContainer.appendChild(createItem(filteredValues[i], i - window.start));\n }\n };\n\n // Filter and re-render values\n const renderValues = (filterText: string) => {\n const caseSensitive = this.config.caseSensitive ?? false;\n const compareFilter = caseSensitive ? filterText : filterText.toLowerCase();\n\n // Filter the unique values - search against formatted display name\n filteredValues = uniqueValues.filter((value) => {\n const displayStr = formatValue(value);\n const compareValue = caseSensitive ? displayStr : displayStr.toLowerCase();\n return !filterText || compareValue.includes(compareFilter);\n });\n\n if (filteredValues.length === 0) {\n spacer.style.height = '0px';\n contentContainer.innerHTML = '';\n const noMatch = document.createElement('div');\n noMatch.className = 'tbw-filter-no-match';\n noMatch.textContent = 'No matching values';\n contentContainer.appendChild(noMatch);\n return;\n }\n\n renderVisibleItems();\n };\n\n // Scroll handler for virtualization\n valuesContainer.addEventListener(\n 'scroll',\n () => {\n if (filteredValues.length > 0) {\n renderVisibleItems();\n }\n },\n { passive: true },\n );\n\n renderValues(searchInput.value);\n panel.appendChild(valuesContainer);\n\n // Debounced search\n let debounceTimer: ReturnType<typeof setTimeout>;\n searchInput.addEventListener('input', () => {\n clearTimeout(debounceTimer);\n debounceTimer = setTimeout(() => {\n this.searchText.set(field, searchInput.value);\n renderValues(searchInput.value);\n }, this.config.debounceMs ?? 150);\n });\n\n // Apply/Clear buttons\n const buttonRow = document.createElement('div');\n buttonRow.className = 'tbw-filter-buttons';\n\n const applyBtn = document.createElement('button');\n applyBtn.className = 'tbw-filter-apply-btn';\n applyBtn.textContent = 'Apply';\n applyBtn.addEventListener('click', () => {\n // Read from checkState map (works with virtualization)\n const excluded: unknown[] = [];\n for (const [key, isChecked] of checkState) {\n if (!isChecked) {\n if (key === '__null__') {\n excluded.push(null);\n } else {\n // Try to match original value type\n const original = uniqueValues.find((v) => String(v) === key);\n excluded.push(original !== undefined ? original : key);\n }\n }\n }\n params.applySetFilter(excluded);\n });\n buttonRow.appendChild(applyBtn);\n\n const clearBtn = document.createElement('button');\n clearBtn.className = 'tbw-filter-clear-btn';\n clearBtn.textContent = 'Clear Filter';\n clearBtn.addEventListener('click', () => {\n params.clearFilter();\n });\n buttonRow.appendChild(clearBtn);\n\n panel.appendChild(buttonRow);\n }\n\n /**\n * Render a number range filter panel with min/max inputs and slider\n */\n private renderNumberFilterPanel(panel: HTMLElement, params: FilterPanelParams, uniqueValues: unknown[]): void {\n const { field, column } = params;\n\n // Get range configuration from filterParams, editorParams, or compute from data\n const filterParams = column.filterParams;\n const editorParams = column.editorParams as { min?: number; max?: number; step?: number } | undefined;\n\n // Helper to convert to number\n const toNumber = (val: unknown, fallback: number): number => {\n if (typeof val === 'number') return val;\n if (typeof val === 'string') {\n const num = parseFloat(val);\n return isNaN(num) ? fallback : num;\n }\n return fallback;\n };\n\n // Compute min/max from data if not specified\n const numericValues = uniqueValues.filter((v) => typeof v === 'number' && !isNaN(v)) as number[];\n const dataMin = numericValues.length > 0 ? Math.min(...numericValues) : 0;\n const dataMax = numericValues.length > 0 ? Math.max(...numericValues) : 100;\n\n const min = toNumber(filterParams?.min ?? editorParams?.min, dataMin);\n const max = toNumber(filterParams?.max ?? editorParams?.max, dataMax);\n const step = filterParams?.step ?? editorParams?.step ?? 1;\n\n // Get current filter values if any\n const currentFilter = this.filters.get(field);\n let currentMin = min;\n let currentMax = max;\n if (currentFilter?.operator === 'between') {\n currentMin = toNumber(currentFilter.value, min);\n currentMax = toNumber(currentFilter.valueTo, max);\n } else if (currentFilter?.operator === 'greaterThanOrEqual') {\n currentMin = toNumber(currentFilter.value, min);\n } else if (currentFilter?.operator === 'lessThanOrEqual') {\n currentMax = toNumber(currentFilter.value, max);\n }\n\n // Range inputs container\n const rangeContainer = document.createElement('div');\n rangeContainer.className = 'tbw-filter-range-inputs';\n\n // Min input\n const minGroup = document.createElement('div');\n minGroup.className = 'tbw-filter-range-group';\n\n const minLabel = document.createElement('label');\n minLabel.textContent = 'Min';\n minLabel.className = 'tbw-filter-range-label';\n\n const minInput = document.createElement('input');\n minInput.type = 'number';\n minInput.className = 'tbw-filter-range-input';\n minInput.min = String(min);\n minInput.max = String(max);\n minInput.step = String(step);\n minInput.value = String(currentMin);\n\n minGroup.appendChild(minLabel);\n minGroup.appendChild(minInput);\n rangeContainer.appendChild(minGroup);\n\n // Separator\n const separator = document.createElement('span');\n separator.className = 'tbw-filter-range-separator';\n separator.textContent = '–';\n rangeContainer.appendChild(separator);\n\n // Max input\n const maxGroup = document.createElement('div');\n maxGroup.className = 'tbw-filter-range-group';\n\n const maxLabel = document.createElement('label');\n maxLabel.textContent = 'Max';\n maxLabel.className = 'tbw-filter-range-label';\n\n const maxInput = document.createElement('input');\n maxInput.type = 'number';\n maxInput.className = 'tbw-filter-range-input';\n maxInput.min = String(min);\n maxInput.max = String(max);\n maxInput.step = String(step);\n maxInput.value = String(currentMax);\n\n maxGroup.appendChild(maxLabel);\n maxGroup.appendChild(maxInput);\n rangeContainer.appendChild(maxGroup);\n\n panel.appendChild(rangeContainer);\n\n // Range slider (dual thumb using two range inputs)\n const sliderContainer = document.createElement('div');\n sliderContainer.className = 'tbw-filter-range-slider';\n\n const sliderTrack = document.createElement('div');\n sliderTrack.className = 'tbw-filter-range-track';\n\n const sliderFill = document.createElement('div');\n sliderFill.className = 'tbw-filter-range-fill';\n\n const minSlider = document.createElement('input');\n minSlider.type = 'range';\n minSlider.className = 'tbw-filter-range-thumb tbw-filter-range-thumb-min';\n minSlider.min = String(min);\n minSlider.max = String(max);\n minSlider.step = String(step);\n minSlider.value = String(currentMin);\n\n const maxSlider = document.createElement('input');\n maxSlider.type = 'range';\n maxSlider.className = 'tbw-filter-range-thumb tbw-filter-range-thumb-max';\n maxSlider.min = String(min);\n maxSlider.max = String(max);\n maxSlider.step = String(step);\n maxSlider.value = String(currentMax);\n\n sliderContainer.appendChild(sliderTrack);\n sliderContainer.appendChild(sliderFill);\n sliderContainer.appendChild(minSlider);\n sliderContainer.appendChild(maxSlider);\n panel.appendChild(sliderContainer);\n\n // Update fill position\n const updateFill = () => {\n const minVal = parseFloat(minSlider.value);\n const maxVal = parseFloat(maxSlider.value);\n const range = max - min;\n const leftPercent = ((minVal - min) / range) * 100;\n const rightPercent = ((maxVal - min) / range) * 100;\n sliderFill.style.left = `${leftPercent}%`;\n sliderFill.style.width = `${rightPercent - leftPercent}%`;\n };\n\n // Sync inputs with sliders\n minSlider.addEventListener('input', () => {\n const val = Math.min(parseFloat(minSlider.value), parseFloat(maxSlider.value));\n minSlider.value = String(val);\n minInput.value = String(val);\n updateFill();\n });\n\n maxSlider.addEventListener('input', () => {\n const val = Math.max(parseFloat(maxSlider.value), parseFloat(minSlider.value));\n maxSlider.value = String(val);\n maxInput.value = String(val);\n updateFill();\n });\n\n // Sync sliders with inputs\n minInput.addEventListener('input', () => {\n let val = parseFloat(minInput.value) || min;\n val = Math.max(min, Math.min(val, parseFloat(maxInput.value)));\n minSlider.value = String(val);\n updateFill();\n });\n\n maxInput.addEventListener('input', () => {\n let val = parseFloat(maxInput.value) || max;\n val = Math.min(max, Math.max(val, parseFloat(minInput.value)));\n maxSlider.value = String(val);\n updateFill();\n });\n\n // Initialize fill\n updateFill();\n\n // Apply/Clear buttons\n const buttonRow = document.createElement('div');\n buttonRow.className = 'tbw-filter-buttons';\n\n const applyBtn = document.createElement('button');\n applyBtn.className = 'tbw-filter-apply-btn';\n applyBtn.textContent = 'Apply';\n applyBtn.addEventListener('click', () => {\n const minVal = parseFloat(minInput.value);\n const maxVal = parseFloat(maxInput.value);\n params.applyTextFilter('between', minVal, maxVal);\n });\n buttonRow.appendChild(applyBtn);\n\n const clearBtn = document.createElement('button');\n clearBtn.className = 'tbw-filter-clear-btn';\n clearBtn.textContent = 'Clear Filter';\n clearBtn.addEventListener('click', () => {\n params.clearFilter();\n });\n buttonRow.appendChild(clearBtn);\n\n panel.appendChild(buttonRow);\n }\n\n /**\n * Render a date range filter panel with from/to date inputs\n */\n private renderDateFilterPanel(panel: HTMLElement, params: FilterPanelParams, uniqueValues: unknown[]): void {\n const { field, column } = params;\n\n // Get range configuration from filterParams, editorParams, or compute from data\n const filterParams = column.filterParams;\n const editorParams = column.editorParams as { min?: string; max?: string } | undefined;\n\n // Compute min/max from data if not specified\n const dateValues = uniqueValues\n .filter((v) => v instanceof Date || (typeof v === 'string' && !isNaN(Date.parse(v))))\n .map((v) => (v instanceof Date ? v : new Date(v as string)))\n .filter((d) => !isNaN(d.getTime()));\n\n const dataMin = dateValues.length > 0 ? new Date(Math.min(...dateValues.map((d) => d.getTime()))) : null;\n const dataMax = dateValues.length > 0 ? new Date(Math.max(...dateValues.map((d) => d.getTime()))) : null;\n\n // Format date for input[type=\"date\"] (YYYY-MM-DD)\n const formatDateForInput = (date: Date | null): string => {\n if (!date) return '';\n return date.toISOString().split('T')[0];\n };\n\n const parseFilterParam = (value: unknown): string => {\n if (!value) return '';\n if (typeof value === 'string') return value;\n if (typeof value === 'number') return formatDateForInput(new Date(value));\n return '';\n };\n\n const minDate =\n parseFilterParam(filterParams?.min) || parseFilterParam(editorParams?.min) || formatDateForInput(dataMin);\n const maxDate =\n parseFilterParam(filterParams?.max) || parseFilterParam(editorParams?.max) || formatDateForInput(dataMax);\n\n // Get current filter values if any\n const currentFilter = this.filters.get(field);\n let currentFrom = '';\n let currentTo = '';\n const isBlankFilter = currentFilter?.operator === 'blank';\n if (currentFilter?.operator === 'between') {\n currentFrom = parseFilterParam(currentFilter.value) || '';\n currentTo = parseFilterParam(currentFilter.valueTo) || '';\n } else if (currentFilter?.operator === 'greaterThanOrEqual') {\n currentFrom = parseFilterParam(currentFilter.value) || '';\n } else if (currentFilter?.operator === 'lessThanOrEqual') {\n currentTo = parseFilterParam(currentFilter.value) || '';\n }\n\n // Date range inputs container\n const rangeContainer = document.createElement('div');\n rangeContainer.className = 'tbw-filter-date-range';\n\n // From input\n const fromGroup = document.createElement('div');\n fromGroup.className = 'tbw-filter-date-group';\n\n const fromLabel = document.createElement('label');\n fromLabel.textContent = 'From';\n fromLabel.className = 'tbw-filter-range-label';\n\n const fromInput = document.createElement('input');\n fromInput.type = 'date';\n fromInput.className = 'tbw-filter-date-input';\n if (minDate) fromInput.min = minDate;\n if (maxDate) fromInput.max = maxDate;\n fromInput.value = currentFrom;\n\n fromGroup.appendChild(fromLabel);\n fromGroup.appendChild(fromInput);\n rangeContainer.appendChild(fromGroup);\n\n // Separator\n const separator = document.createElement('span');\n separator.className = 'tbw-filter-range-separator';\n separator.textContent = '–';\n rangeContainer.appendChild(separator);\n\n // To input\n const toGroup = document.createElement('div');\n toGroup.className = 'tbw-filter-date-group';\n\n const toLabel = document.createElement('label');\n toLabel.textContent = 'To';\n toLabel.className = 'tbw-filter-range-label';\n\n const toInput = document.createElement('input');\n toInput.type = 'date';\n toInput.className = 'tbw-filter-date-input';\n if (minDate) toInput.min = minDate;\n if (maxDate) toInput.max = maxDate;\n toInput.value = currentTo;\n\n toGroup.appendChild(toLabel);\n toGroup.appendChild(toInput);\n rangeContainer.appendChild(toGroup);\n\n panel.appendChild(rangeContainer);\n\n // \"Show only blank\" checkbox\n const blankRow = document.createElement('label');\n blankRow.className = 'tbw-filter-blank-option';\n\n const blankCheckbox = document.createElement('input');\n blankCheckbox.type = 'checkbox';\n blankCheckbox.className = 'tbw-filter-blank-checkbox';\n blankCheckbox.checked = isBlankFilter;\n\n const blankLabel = document.createTextNode('Show only blank');\n blankRow.appendChild(blankCheckbox);\n blankRow.appendChild(blankLabel);\n\n // Toggle date inputs disabled state when blank is checked\n const toggleDateInputs = (disabled: boolean): void => {\n fromInput.disabled = disabled;\n toInput.disabled = disabled;\n rangeContainer.classList.toggle('tbw-filter-disabled', disabled);\n };\n toggleDateInputs(isBlankFilter);\n\n blankCheckbox.addEventListener('change', () => {\n toggleDateInputs(blankCheckbox.checked);\n });\n\n panel.appendChild(blankRow);\n\n // Apply/Clear buttons\n const buttonRow = document.createElement('div');\n buttonRow.className = 'tbw-filter-buttons';\n\n const applyBtn = document.createElement('button');\n applyBtn.className = 'tbw-filter-apply-btn';\n applyBtn.textContent = 'Apply';\n applyBtn.addEventListener('click', () => {\n if (blankCheckbox.checked) {\n params.applyTextFilter('blank', '');\n return;\n }\n\n const from = fromInput.value;\n const to = toInput.value;\n\n if (from && to) {\n params.applyTextFilter('between', from, to);\n } else if (from) {\n params.applyTextFilter('greaterThanOrEqual', from);\n } else if (to) {\n params.applyTextFilter('lessThanOrEqual', to);\n } else {\n params.clearFilter();\n }\n });\n buttonRow.appendChild(applyBtn);\n\n const clearBtn = document.createElement('button');\n clearBtn.className = 'tbw-filter-clear-btn';\n clearBtn.textContent = 'Clear Filter';\n clearBtn.addEventListener('click', () => {\n params.clearFilter();\n });\n buttonRow.appendChild(clearBtn);\n\n panel.appendChild(buttonRow);\n }\n\n /**\n * Apply a set filter (exclude values)\n */\n private applySetFilter(field: string, excluded: unknown[]): void {\n // Store excluded values\n this.excludedValues.set(field, new Set(excluded));\n\n if (excluded.length === 0) {\n // No exclusions = no filter\n this.filters.delete(field);\n } else {\n // Create \"notIn\" filter\n this.filters.set(field, {\n field,\n type: 'set',\n operator: 'notIn',\n value: excluded,\n });\n }\n\n this.applyFiltersInternal();\n }\n\n /**\n * Apply a text/number/date filter\n */\n private applyTextFilter(\n field: string,\n operator: FilterModel['operator'],\n value: string | number,\n valueTo?: string | number,\n ): void {\n this.filters.set(field, {\n field,\n type: 'text',\n operator,\n value,\n valueTo,\n });\n\n this.applyFiltersInternal();\n }\n\n /**\n * Internal method to apply filters (sync or async based on config)\n */\n private applyFiltersInternal(): void {\n this.cachedResult = null;\n this.cacheKey = null;\n this.cachedInputSpot = null;\n\n const filterList = [...this.filters.values()];\n\n // If using async filterHandler, delegate to server\n if (this.config.filterHandler) {\n const gridEl = this.grid as unknown as Element;\n gridEl.setAttribute('aria-busy', 'true');\n\n const result = this.config.filterHandler(filterList, this.sourceRows as unknown[]);\n\n // Handle async or sync result\n const handleResult = (rows: unknown[]) => {\n gridEl.removeAttribute('aria-busy');\n this.cachedResult = rows;\n\n // Update grid rows directly for async filtering\n (this.grid as unknown as { rows: unknown[] }).rows = rows;\n\n this.emit<FilterChangeDetail>('filter-change', {\n filters: filterList,\n filteredRowCount: rows.length,\n selected: this.computeSelected(),\n });\n // Notify other plugins via Event Bus\n this.emitPluginEvent('filter-applied', { filters: filterList });\n\n // Trigger afterRender to update filter button active state\n this.requestRender();\n };\n\n if (result && typeof (result as Promise<unknown[]>).then === 'function') {\n (result as Promise<unknown[]>).then(handleResult);\n } else {\n handleResult(result as unknown[]);\n }\n return;\n }\n\n // Sync path: emit event and re-render (processRows will handle filtering)\n this.emit<FilterChangeDetail>('filter-change', {\n filters: filterList,\n filteredRowCount: 0,\n selected: this.computeSelected(),\n });\n // Notify other plugins via Event Bus\n this.emitPluginEvent('filter-applied', { filters: filterList });\n this.requestRender();\n }\n // #endregion\n\n // #region Column State Hooks\n\n /**\n * Return filter state for a column if it has an active filter.\n * @internal\n */\n override getColumnState(field: string): Partial<ColumnState> | undefined {\n const filterModel = this.filters.get(field);\n if (!filterModel) return undefined;\n\n return {\n filter: {\n type: filterModel.type,\n operator: filterModel.operator,\n value: filterModel.value,\n valueTo: filterModel.valueTo,\n },\n };\n }\n\n /**\n * Apply filter state from column state.\n * @internal\n */\n override applyColumnState(field: string, state: ColumnState): void {\n // Only process if the column has filter state\n if (!state.filter) {\n this.filters.delete(field);\n return;\n }\n\n // Reconstruct the FilterModel from the stored state\n const filterModel: FilterModel = {\n field,\n type: state.filter.type,\n operator: state.filter.operator as FilterModel['operator'],\n value: state.filter.value,\n valueTo: state.filter.valueTo,\n };\n\n this.filters.set(field, filterModel);\n // Invalidate cache so filter is reapplied\n this.cachedResult = null;\n this.cacheKey = null;\n this.cachedInputSpot = null;\n }\n // #endregion\n}\n"],"names":["BLANK_FILTER_VALUE","toNumeric","value","n","matchesFilter","row","filter","caseSensitive","filterValue","rawValue","extracted","values","excluded","v","included","stringValue","compareValue","compareFilterValue","filterRows","rows","filters","filterValues","f","computeFilterCacheKey","getUniqueValues","field","hasBlank","cellValue","a","b","getUniqueValuesBatch","fields","acc","entry","result","hasExtractor","computeVirtualWindow","params","totalRows","viewportHeight","scrollTop","rowHeight","overscan","visibleRows","start","end","shouldBypassVirtualization","threshold","DEFAULT_FILTER_ICON","DEFAULT_GRID_ICONS","BaseGridPlugin","#abortController","config","grid","PluginClass","eventName","detail","event","eventType","callback","userIcons","mode","host","durationStr","parsed","iconKey","pluginOverride","element","icon","message","isUtilityColumn","column","FilteringPlugin","styles","col","columns","map","cssValue","setFields","c","uniqueMap","selected","unique","query","items","fieldFiltered","hasAnyFilter","filterList","newCacheKey","inputSpot","inputUnchanged","gridEl","cell","colIndex","hasFilter","filterBtn","wasActive","iconName","e","resizeHandle","fullFilter","getter","panel","className","theme","style","filterPanelStyles","buttonEl","uniqueValues","excludedSet","currentSearchText","operator","valueTo","usedCustomRenderer","typeDefault","columnType","anchorEl","panelRect","anchorRect","rect","excludedValues","itemHeight","formatValue","formatted","searchContainer","searchInput","actionsRow","selectAllLabel","selectAllCheckbox","selectAllText","updateSelectAllState","checkState","allChecked","noneChecked","newState","key","renderVisibleItems","valuesContainer","spacer","contentContainer","filteredValues","createItem","index","displayValue","item","checkbox","label","totalItems","idx","window","i","renderValues","filterText","compareFilter","displayStr","noMatch","debounceTimer","buttonRow","applyBtn","isChecked","original","clearBtn","filterParams","editorParams","toNumber","val","fallback","num","numericValues","dataMin","dataMax","min","max","step","currentFilter","currentMin","currentMax","rangeContainer","minGroup","minLabel","minInput","separator","maxGroup","maxLabel","maxInput","sliderContainer","sliderTrack","sliderFill","minSlider","maxSlider","updateFill","minVal","maxVal","range","leftPercent","rightPercent","dateValues","d","formatDateForInput","date","parseFilterParam","minDate","maxDate","currentFrom","currentTo","isBlankFilter","fromGroup","fromLabel","fromInput","toGroup","toLabel","toInput","blankRow","blankCheckbox","blankLabel","toggleDateInputs","disabled","from","to","handleResult","filterModel","state"],"mappings":"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"}