@toolbox-web/grid 3.8.1 → 3.8.3

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 (82) hide show
  1. package/all.js +1 -1
  2. package/all.js.map +1 -1
  3. package/index.js +1 -1
  4. package/index.js.map +1 -1
  5. package/lib/core/defaults.d.ts +1 -1
  6. package/lib/core/internal/aria.d.ts +1 -1
  7. package/lib/core/internal/config-manager.d.ts +0 -4
  8. package/lib/core/types.d.ts +6 -0
  9. package/lib/features/registry.js +1 -1
  10. package/lib/plugins/clipboard/clipboard-plugin.d.ts +2 -3
  11. package/lib/plugins/clipboard/index.js +1 -1
  12. package/lib/plugins/clipboard/index.js.map +1 -1
  13. package/lib/plugins/column-virtualization/index.js +1 -1
  14. package/lib/plugins/column-virtualization/index.js.map +1 -1
  15. package/lib/plugins/context-menu/index.js +1 -1
  16. package/lib/plugins/context-menu/index.js.map +1 -1
  17. package/lib/plugins/editing/index.js +1 -1
  18. package/lib/plugins/editing/index.js.map +1 -1
  19. package/lib/plugins/export/index.js +1 -1
  20. package/lib/plugins/export/index.js.map +1 -1
  21. package/lib/plugins/filtering/index.js +1 -1
  22. package/lib/plugins/filtering/index.js.map +1 -1
  23. package/lib/plugins/grouping-columns/index.js +1 -1
  24. package/lib/plugins/grouping-columns/index.js.map +1 -1
  25. package/lib/plugins/grouping-rows/index.js +1 -1
  26. package/lib/plugins/grouping-rows/index.js.map +1 -1
  27. package/lib/plugins/master-detail/index.js +1 -1
  28. package/lib/plugins/master-detail/index.js.map +1 -1
  29. package/lib/plugins/multi-sort/index.js +1 -1
  30. package/lib/plugins/multi-sort/index.js.map +1 -1
  31. package/lib/plugins/pinned-columns/index.js +1 -1
  32. package/lib/plugins/pinned-columns/index.js.map +1 -1
  33. package/lib/plugins/pinned-rows/index.js +1 -1
  34. package/lib/plugins/pinned-rows/index.js.map +1 -1
  35. package/lib/plugins/pivot/index.js +1 -1
  36. package/lib/plugins/pivot/index.js.map +1 -1
  37. package/lib/plugins/print/index.js +1 -1
  38. package/lib/plugins/print/index.js.map +1 -1
  39. package/lib/plugins/reorder-columns/index.js +1 -1
  40. package/lib/plugins/reorder-columns/index.js.map +1 -1
  41. package/lib/plugins/responsive/index.js +1 -1
  42. package/lib/plugins/responsive/index.js.map +1 -1
  43. package/lib/plugins/row-drag-drop/index.js +1 -1
  44. package/lib/plugins/row-drag-drop/index.js.map +1 -1
  45. package/lib/plugins/row-drag-drop/row-drag-drop-plugin.d.ts +1 -0
  46. package/lib/plugins/row-drag-drop/types.d.ts +3 -2
  47. package/lib/plugins/selection/index.js +1 -1
  48. package/lib/plugins/selection/index.js.map +1 -1
  49. package/lib/plugins/selection/selection-plugin.d.ts +2 -2
  50. package/lib/plugins/server-side/index.js +1 -1
  51. package/lib/plugins/server-side/index.js.map +1 -1
  52. package/lib/plugins/shell/index.js +1 -1
  53. package/lib/plugins/shell/index.js.map +1 -1
  54. package/lib/plugins/sticky-rows/index.js +1 -1
  55. package/lib/plugins/sticky-rows/index.js.map +1 -1
  56. package/lib/plugins/tooltip/index.js +1 -1
  57. package/lib/plugins/tooltip/index.js.map +1 -1
  58. package/lib/plugins/tree/index.js +1 -1
  59. package/lib/plugins/tree/index.js.map +1 -1
  60. package/lib/plugins/undo-redo/index.js +1 -1
  61. package/lib/plugins/undo-redo/index.js.map +1 -1
  62. package/lib/plugins/visibility/index.js +1 -1
  63. package/lib/plugins/visibility/index.js.map +1 -1
  64. package/package.json +1 -1
  65. package/umd/grid.all.umd.js +1 -1
  66. package/umd/grid.all.umd.js.map +1 -1
  67. package/umd/grid.umd.js +1 -1
  68. package/umd/grid.umd.js.map +1 -1
  69. package/umd/plugins/clipboard.umd.js +1 -1
  70. package/umd/plugins/clipboard.umd.js.map +1 -1
  71. package/umd/plugins/editing.umd.js +1 -1
  72. package/umd/plugins/editing.umd.js.map +1 -1
  73. package/umd/plugins/grouping-rows.umd.js +1 -1
  74. package/umd/plugins/grouping-rows.umd.js.map +1 -1
  75. package/umd/plugins/row-drag-drop.umd.js +1 -1
  76. package/umd/plugins/row-drag-drop.umd.js.map +1 -1
  77. package/umd/plugins/selection.umd.js +1 -1
  78. package/umd/plugins/selection.umd.js.map +1 -1
  79. package/umd/plugins/tooltip.umd.js +1 -1
  80. package/umd/plugins/tooltip.umd.js.map +1 -1
  81. package/umd/plugins/tree.umd.js +1 -1
  82. package/umd/plugins/tree.umd.js.map +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../../../../../../libs/grid/src/lib/core/constants.ts","../../../../../../libs/grid/src/lib/core/internal/value-accessor.ts","../../../../../../libs/grid/src/lib/core/internal/aggregators.ts","../../../../../../libs/grid/src/lib/core/defaults.ts","../../../../../../libs/grid/src/lib/core/internal/aria.ts","../../../../../../libs/grid/src/lib/core/internal/sanitize.ts","../../../../../../libs/grid/src/lib/core/internal/loading.ts","../../../../../../libs/grid/src/lib/core/internal/diagnostics.ts","../../../../../../libs/grid/src/lib/core/plugin/base-plugin.ts","../../../../../../libs/grid/src/lib/core/plugin/expander-column.ts","../../../../../../libs/grid/src/lib/plugins/grouping-rows/grouping-rows.ts","../../../../../../libs/grid/src/lib/plugins/grouping-rows/grouping-rows.css?inline","../../../../../../libs/grid/src/lib/plugins/grouping-rows/grouping-rows-plugin.ts"],"sourcesContent":["/**\n * Grid DOM Constants\n *\n * Centralized constants for CSS classes, data attributes, and selectors\n * used throughout the grid. Use these instead of magic strings.\n *\n * Note: Some constants here are used by plugins but defined in core because:\n * 1. The core CSS needs to style these elements (e.g., sticky positioning)\n * 2. Multiple plugins may share the same class names\n *\n * Plugins can define their own additional constants and export them.\n * See each plugin's index.ts for plugin-specific exports.\n */\n\n/**\n * CSS selector for focusable editor elements within a cell.\n * Used by EditingPlugin, UndoRedoPlugin, and keyboard navigation. Defined here\n * (a leaf module) rather than in `rows.ts` so `keyboard.ts` can consume it\n * without creating a rows ↔ keyboard import cycle.\n */\nexport const FOCUSABLE_EDITOR_SELECTOR =\n 'input,select,textarea,[contenteditable=\"true\"],[contenteditable=\"\"],[tabindex]:not([tabindex=\"-1\"])';\n\n// #region CSS Classes\n\n/**\n * CSS class names used in the grid's DOM.\n * Use these when adding/removing classes or querying elements.\n *\n * Classes are organized by:\n * - Core: Used by the grid core for structure and basic functionality\n * - Shared: Used by multiple features/plugins, styled by core CSS\n *\n * @category Plugin Development\n * @since 0.1.1\n */\nexport const GridClasses = {\n // ─── Core Structure ───────────────────────────────────────────────\n ROOT: 'tbw-grid-root',\n HEADER: 'header',\n HEADER_ROW: 'header-row',\n HEADER_CELL: 'header-cell',\n\n // Body structure\n ROWS_VIEWPORT: 'rows-viewport',\n ROWS_SPACER: 'rows-spacer',\n ROWS_CONTAINER: 'rows',\n\n // Row elements\n DATA_ROW: 'data-grid-row',\n GROUP_ROW: 'group-row',\n\n // Cell elements\n DATA_CELL: 'cell',\n\n // ─── Core States ──────────────────────────────────────────────────\n SELECTED: 'selected',\n FOCUSED: 'focused',\n EDITING: 'editing',\n EXPANDED: 'expanded',\n COLLAPSED: 'collapsed',\n DRAGGING: 'dragging',\n RESIZING: 'resizing',\n\n // Sorting (core feature)\n SORTABLE: 'sortable',\n SORTED_ASC: 'sorted-asc',\n SORTED_DESC: 'sorted-desc',\n\n // Visibility\n HIDDEN: 'hidden',\n\n // ─── Shared Classes (used by plugins, styled by core CSS) ────────\n // These are defined here because core CSS provides the base styling.\n // Plugins apply these classes; core CSS defines how they look.\n\n // Sticky positioning (PinnedColumnsPlugin applies, core CSS styles)\n STICKY_LEFT: 'sticky-left',\n STICKY_RIGHT: 'sticky-right',\n\n // Pinned rows (PinnedRowsPlugin applies, core CSS styles)\n PINNED_TOP: 'pinned-top',\n PINNED_BOTTOM: 'pinned-bottom',\n\n // Tree structure (TreePlugin applies, core CSS styles)\n TREE_TOGGLE: 'tree-toggle',\n TREE_INDENT: 'tree-indent',\n\n // Grouping (GroupingRowsPlugin applies, core CSS styles)\n GROUP_TOGGLE: 'group-toggle',\n GROUP_LABEL: 'group-label',\n GROUP_COUNT: 'group-count',\n\n // Selection (SelectionPlugin applies, core CSS styles)\n RANGE_SELECTION: 'range-selection',\n SELECTION_OVERLAY: 'selection-overlay',\n} as const;\n\n// #endregion\n\n// #region Data Attributes\n\n/**\n * Data attribute names used on grid elements.\n * Use these when getting/setting data attributes.\n *\n * @category Plugin Development\n * @since 0.1.1\n */\nexport const GridDataAttrs = {\n // ─── Core Attributes ──────────────────────────────────────────────\n FIELD: 'data-field',\n\n /**\n * Marks a `title` the grid itself put on a cell to reveal ellipsised text\n * (WCAG SC 1.4.12). Anything that sets its own `title` on a cell must clear\n * this, or the grid will later mistake that title for its own and replace it.\n *\n * @since 3.6.0\n */\n TRUNCATED: 'data-tbw-truncated',\n\n // ─── Shared Attributes (used by plugins) ──────────────────────────\n GROUP_KEY: 'data-group-key', // GroupingRowsPlugin\n} as const;\n\n// #endregion\n\n// #region Selectors\n\n/**\n * Common CSS selectors for querying grid elements.\n * Built from the class constants for consistency.\n *\n * @category Plugin Development\n * @since 0.1.1\n */\nexport const GridSelectors = {\n ROOT: `.${GridClasses.ROOT}`,\n HEADER: `.${GridClasses.HEADER}`,\n HEADER_ROW: `.${GridClasses.HEADER_ROW}`,\n HEADER_CELL: `.${GridClasses.HEADER_CELL}`,\n ROWS_VIEWPORT: `.${GridClasses.ROWS_VIEWPORT}`,\n ROWS_CONTAINER: `.${GridClasses.ROWS_CONTAINER}`,\n DATA_ROW: `.${GridClasses.DATA_ROW}`,\n DATA_CELL: `.${GridClasses.DATA_CELL}`,\n GROUP_ROW: `.${GridClasses.GROUP_ROW}`,\n\n // By data attribute\n CELL_BY_FIELD: (field: string) => `.${GridClasses.DATA_CELL}[${GridDataAttrs.FIELD}=\"${field}\"]`,\n\n // State selectors\n SELECTED_ROWS: `.${GridClasses.DATA_ROW}.${GridClasses.SELECTED}`,\n EDITING_CELL: `.${GridClasses.DATA_CELL}.${GridClasses.EDITING}`,\n} as const;\n\n// #endregion\n\n// #region CSS Custom Properties\n\n/**\n * CSS custom property names for theming.\n * Use these when programmatically setting styles.\n *\n * @category Plugin Development\n * @since 0.1.1\n */\nexport const GridCSSVars = {\n // Colors\n COLOR_BG: '--tbw-color-bg',\n COLOR_FG: '--tbw-color-fg',\n COLOR_FG_MUTED: '--tbw-color-fg-muted',\n COLOR_BORDER: '--tbw-color-border',\n COLOR_ACCENT: '--tbw-color-accent',\n COLOR_HEADER_BG: '--tbw-color-header-bg',\n COLOR_HEADER_FG: '--tbw-color-header-fg',\n COLOR_SELECTION: '--tbw-color-selection',\n COLOR_ROW_HOVER: '--tbw-color-row-hover',\n COLOR_ROW_ALT: '--tbw-color-row-alt',\n\n // Sizing\n ROW_HEIGHT: '--tbw-row-height',\n HEADER_HEIGHT: '--tbw-header-height',\n CELL_PADDING: '--tbw-cell-padding',\n\n // Typography\n FONT_FAMILY: '--tbw-font-family',\n FONT_SIZE: '--tbw-font-size',\n\n // Borders\n BORDER_RADIUS: '--tbw-border-radius',\n FOCUS_OUTLINE: '--tbw-focus-outline',\n} as const;\n\n// #endregion\n\n// Type helpers\n\n/**\n * Union of all CSS class names used in the grid's DOM.\n * Use `GridClasses` to access individual values by key.\n *\n * @category Plugin Development\n * @since 0.1.1\n */\nexport type GridClassName = (typeof GridClasses)[keyof typeof GridClasses];\n\n/**\n * Union of all `data-*` attribute names used on grid elements.\n * Use `GridDataAttrs` to access individual values by key.\n *\n * @category Plugin Development\n * @since 0.1.1\n */\nexport type GridDataAttr = (typeof GridDataAttrs)[keyof typeof GridDataAttrs];\n\n/**\n * Union of all CSS custom property names (`--tbw-*`) available for theming.\n * Use `GridCSSVars` to access individual values by key.\n *\n * @category Plugin Development\n * @since 0.1.1\n */\nexport type GridCSSVar = (typeof GridCSSVars)[keyof typeof GridCSSVars];\n","/**\n * Value Accessor Module\n *\n * Single source of truth for resolving a column's value from a row.\n *\n * Resolution precedence:\n * 1. `column.valueAccessor({ row, column, rowIndex })` — when defined\n * 2. `row[column.field]` — direct field read (default)\n *\n * Per-column escape hatches (`sortComparator`, `filterValue`) still take\n * precedence over the accessor — the accessor is the *default* value source,\n * never an override. See {@link BaseColumnConfig.valueAccessor}.\n *\n * Performance: results are memoized per (row identity, column field) using a\n * `WeakMap`. Row identity changes (immutable updates) invalidate naturally;\n * in-place mutations must call {@link invalidateAccessorCache} on the row.\n */\n\nimport type { ColumnConfig } from '../types';\n\n// Box-wrapped values let the cache-hit path do a SINGLE `Map.get` instead of\n// `has()` + `get()`. A truthy box means cache hit (even when the cached value\n// itself is `undefined` / `null` / `0` / `''`).\ninterface CacheBox {\n v: unknown;\n}\nconst accessorCache = new WeakMap<object, Map<string, CacheBox>>();\n\n// #region Nested dotted-path field access (issue #438)\n\n// Prototype-pollution guard applied to every path segment on read AND write.\n// Uses explicit `===` comparisons (not a Set lookup) so static analyzers\n// recognize the barrier guarding the assignments in `setByPath`/`writeCellField`.\nfunction isUnsafeKey(key: string): boolean {\n return key === '__proto__' || key === 'constructor' || key === 'prototype';\n}\n\n// Parsed dotted-path cache: field string → segments. Only **dotted** fields are\n// cached; a plain key is recognised by a single `indexOf` scan, which is cheaper\n// than hashing the string for a Map probe and — critically — keeps the cache\n// from growing without bound when a plugin mints synthetic field names at\n// runtime (PivotPlugin derives one column per distinct pivot value).\nconst fieldPathCache = new Map<string, readonly string[]>();\n\n/**\n * Parse a column `field` into path segments, or `null` when it is a plain\n * top-level key (contains no `.`). Dotted results are memoized.\n *\n * @param field - The column field key (may be a dotted path like `a.b.c`).\n * @returns Frozen segment array for dotted paths, else `null`.\n * @since 3.3.0\n */\nexport function parseFieldPath(field: string): readonly string[] | null {\n if (field.indexOf('.') === -1) return null;\n let segments = fieldPathCache.get(field);\n if (segments === undefined) {\n segments = Object.freeze(field.split('.'));\n fieldPathCache.set(field, segments);\n }\n return segments;\n}\n\n/**\n * Read a nested value from `row` by pre-parsed path segments. Returns\n * `undefined` on any nullish/non-object hop. Prototype-pollution safe.\n *\n * @since 3.3.0\n */\nexport function getByPath(row: unknown, path: readonly string[]): unknown {\n let cur = row as Record<string, unknown> | null | undefined;\n for (let i = 0; i < path.length; i++) {\n if (cur == null || typeof cur !== 'object') return undefined;\n const seg = path[i];\n if (isUnsafeKey(seg)) return undefined;\n cur = cur[seg] as Record<string, unknown>;\n }\n return cur;\n}\n\n/**\n * Write `value` into `row` at the location described by pre-parsed path\n * segments. Walks **existing** objects only — it does NOT fabricate missing\n * intermediates — and returns `true` only when the write happened.\n * Prototype-pollution safe (every segment is guarded).\n *\n * @since 3.3.0\n */\nexport function setByPath(row: unknown, path: readonly string[], value: unknown): boolean {\n if (row == null || typeof row !== 'object') return false;\n let cur = row as Record<string, unknown>;\n for (let i = 0; i < path.length - 1; i++) {\n const seg = path[i];\n if (isUnsafeKey(seg)) return false;\n const next = cur[seg];\n if (next == null || typeof next !== 'object') return false;\n cur = next as Record<string, unknown>;\n }\n const last = path[path.length - 1];\n if (isUnsafeKey(last)) return false;\n cur[last] = value;\n return true;\n}\n\n/**\n * Read the raw stored value for `field` from `row`, honoring nested dotted\n * paths — the non-accessor counterpart to {@link resolveCellValue}.\n *\n * Resolution rules (back-compat critical, see `grid-core.md` DECIDED #438):\n * 1. Plain field (no `.`) → direct `row[field]` (identical to legacy path),\n * except prototype-polluting keys (`__proto__`/`constructor`/`prototype`)\n * which return `undefined` — symmetric with {@link writeCellField}.\n * 2. Dotted field whose literal key is an **own property** of `row` → that\n * flat value wins (preserves synthetic-key rows that really store `'a.b'`).\n * 3. Otherwise → nested traversal via {@link getByPath}.\n *\n * @since 3.3.0\n */\nexport function readCellField(row: unknown, field: string): unknown {\n if (row == null) return undefined;\n const path = parseFieldPath(field);\n const r = row as Record<string, unknown>;\n if (path === null) return isUnsafeKey(field) ? undefined : r[field];\n if (Object.prototype.hasOwnProperty.call(r, field)) return r[field];\n return getByPath(r, path);\n}\n\n/**\n * A precompiled reader that extracts one column's raw value from a row.\n *\n * @since 3.4.0\n */\nexport type FieldReader = (row: unknown) => unknown;\n\n/**\n * Build a {@link FieldReader} for `field`, resolving the plain / unsafe /\n * dotted decision **once** instead of on every cell read.\n *\n * Hoist the reader out of any per-row loop (sort comparators, compiled filter\n * predicates, aggregators, export writers). Semantics are identical to\n * {@link readCellField}; only the dispatch is paid up front.\n *\n * @param field - The column field key (may be a dotted path like `a.b.c`).\n * @returns A single-argument reader closure.\n *\n * @example\n * ```typescript\n * const read = createFieldReader(column.field);\n * rows.sort((a, b) => compare(read(a), read(b)));\n * ```\n * @since 3.4.0\n */\nexport function createFieldReader(field: string): FieldReader {\n const path = parseFieldPath(field);\n if (path === null) {\n if (isUnsafeKey(field)) return () => undefined;\n return (row) => (row == null ? undefined : (row as Record<string, unknown>)[field]);\n }\n return (row) => {\n if (row == null) return undefined;\n const r = row as Record<string, unknown>;\n // A literal own key containing a dot wins over traversal (see readCellField).\n if (Object.prototype.hasOwnProperty.call(r, field)) return r[field];\n return getByPath(r, path);\n };\n}\n\n/**\n * Whether `field` is a plain, safe top-level key — i.e. `row[field]` is exactly\n * equivalent to {@link readCellField} and callers may inline the property read.\n *\n * Used by benchmarked hot loops (compiled filter predicates) that keep a\n * hand-inlined fast path and only fall back to a {@link FieldReader} for the\n * dotted/unsafe minority.\n *\n * @since 3.4.0\n */\nexport function isPlainField(field: string): boolean {\n return field.indexOf('.') === -1 && !isUnsafeKey(field);\n}\n\n/**\n * Write `value` into `row` for `field`, honoring nested dotted paths and the\n * same back-compat precedence as {@link readCellField}. Returns `true` when a\n * write occurred. Prototype-pollution safe.\n *\n * @since 3.3.0\n */\nexport function writeCellField(row: unknown, field: string, value: unknown): boolean {\n if (row == null || typeof row !== 'object') return false;\n const path = parseFieldPath(field);\n const r = row as Record<string, unknown>;\n if (path === null) {\n if (isUnsafeKey(field)) return false;\n r[field] = value;\n return true;\n }\n // A literal own key containing a dot wins over traversal (symmetric with\n // readCellField). Such a key always contains `.`, so it can never be an\n // unsafe prototype key.\n if (Object.prototype.hasOwnProperty.call(r, field)) {\n r[field] = value;\n return true;\n }\n return setByPath(r, path, value);\n}\n\n// #endregion\n\n/**\n * Resolve the cell value for a column, honoring `valueAccessor` if defined,\n * otherwise reading `row[column.field]` (with nested dotted-path support).\n *\n * @param row - The row object\n * @param column - The column definition\n * @param rowIndex - The visible row index (passed to accessor; defaults to -1)\n * @returns The resolved cell value\n *\n * @example\n * ```typescript\n * const value = resolveCellValue(row, column);\n * ```\n * @since 2.2.0\n */\nexport function resolveCellValue<TRow>(row: TRow, column: ColumnConfig<TRow>, rowIndex = -1): unknown {\n if (!column.valueAccessor) {\n return readCellField(row, column.field);\n }\n // Non-object rows (primitives, null) bypass the cache.\n if (typeof row !== 'object' || row === null) {\n return column.valueAccessor({ row, column, rowIndex });\n }\n // After the guard above, `row` is provably a non-null object. Single\n // assertion (no `as unknown as`) bridges the generic `TRow` to the WeakMap\n // key type; see `.github/instructions/typescript-conventions.instructions.md`.\n const rowKey = row as object;\n const key = column.field;\n let cellMap = accessorCache.get(rowKey);\n if (cellMap) {\n const box = cellMap.get(key);\n if (box !== undefined) return box.v;\n } else {\n cellMap = new Map();\n accessorCache.set(rowKey, cellMap);\n }\n const value = column.valueAccessor({ row, column, rowIndex });\n cellMap.set(key, { v: value });\n return value;\n}\n\n/**\n * Invalidate cached accessor values for a row (after in-place mutation),\n * a single (row, field) pair, or — when called with no argument — clear\n * the entire cache. Immutable updates auto-invalidate via row identity.\n *\n * Edit/transaction paths that mutate row objects in-place must call this.\n * @since 2.2.0\n */\nexport function invalidateAccessorCache(row?: object, field?: string): void {\n if (!row) {\n // WeakMap can't be cleared in O(1); the GC handles unreferenced rows.\n // For an explicit full reset, callers should mutate row identities instead.\n return;\n }\n if (!field) {\n accessorCache.delete(row);\n return;\n }\n accessorCache.get(row)?.delete(field);\n}\n\n/**\n * Build a `(value, row) => unknown` extractor compatible with\n * `filterValue`-style APIs from a column's `valueAccessor`. Returns `undefined`\n * when the column has no accessor.\n *\n * Used by FilteringPlugin to bridge the two API shapes without duplicating\n * cache lookups.\n */\nexport function accessorAsFilterValue<TRow>(\n column: ColumnConfig<TRow>,\n): ((value: unknown, row: TRow) => unknown) | undefined {\n if (!column.valueAccessor) return undefined;\n return (_value, row) => resolveCellValue(row, column);\n}\n","/**\n * Aggregators Core Registry\n *\n * Provides a central registry for aggregator functions.\n * Built-in aggregators are provided by default.\n * Plugins can register additional aggregators.\n *\n * The registry is exposed as a singleton object that can be accessed:\n * - By ES module imports: import { aggregatorRegistry } from '@toolbox-web/grid'\n * - By UMD/CDN: TbwGrid.aggregatorRegistry\n * - By plugins via context: ctx.aggregatorRegistry\n */\n\n/* eslint-disable @typescript-eslint/no-explicit-any */\n\nimport { readCellField, resolveCellValue } from './value-accessor';\n\n/**\n * Aggregation function as stored in the core registry.\n *\n * Loosely typed (`any`) on purpose: the registry holds aggregators contributed\n * by any plugin for any row shape. `plugins/pinned-rows/types.ts` declares a\n * same-named but *stricter* public alias (`unknown[]` / `ColumnConfig`) for\n * users authoring an aggregator. The two must stay separate — tightening this\n * one breaks registry storage, loosening that one loses user-facing type\n * safety.\n */\nexport type AggregatorFn = (rows: any[], field: string, column?: any) => any;\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 plugin-registered aggregator name\n * - A custom {@link AggregatorFn}\n *\n * Aggregators are not declared on a column directly — they are configured per\n * field on the consumer (e.g. `RowGroupRenderConfig.aggregators` for group\n * rows, keyed by column field name).\n *\n * @example\n * ```typescript\n * // Built-in aggregator on a group row config\n * { aggregators: { amount: 'sum' } }\n *\n * // Custom aggregator function\n * {\n * aggregators: {\n * price: (rows, field) => {\n * const values = rows.map((r) => (r as any)[field]).filter((v) => v != null);\n * return values.length ? Math.max(...(values as number[])) : null;\n * },\n * },\n * }\n * ```\n *\n * @since 0.1.1\n */\nexport type AggregatorRef = string | AggregatorFn;\n\n/**\n * Read a cell for an aggregator. When the column has a `valueAccessor`,\n * route through it (single source of truth — see issue #230); otherwise\n * fall back to a direct field read for the fast path.\n */\nfunction readCell(row: any, field: string, column?: any): unknown {\n if (column?.valueAccessor) return resolveCellValue(row, column);\n return readCellField(row, field);\n}\n\n/**\n * Check whether a cell value should be treated as blank and skipped by numeric\n * aggregators. Matches the semantics used by the filter engine:\n * `null` / `undefined` / `''` / `NaN` are all considered absent values.\n *\n * Without this guard, `Number('')` coerces to `0` and `null` coerces to `0`,\n * which silently pulls blank cells into sums, averages, and min/max comparisons\n * (a blank cell would otherwise \"win\" a min/max against any positive/negative\n * dataset). Skipping blanks mirrors how Excel treats empty cells in SUM/AVG/MIN/MAX.\n */\nfunction isBlankCell(v: unknown): boolean {\n return v == null || v === '' || (typeof v === 'number' && isNaN(v));\n}\n\n/** Built-in aggregator functions */\nconst builtInAggregators: Record<string, AggregatorFn> = {\n sum: (rows, field, column) => {\n let sum = 0;\n for (let i = 0; i < rows.length; i++) {\n const raw = readCell(rows[i], field, column);\n if (isBlankCell(raw)) continue;\n const n = Number(raw);\n if (!isNaN(n)) sum += n;\n }\n return sum;\n },\n avg: (rows, field, column) => {\n if (!rows.length) return 0;\n let sum = 0;\n let count = 0;\n for (let i = 0; i < rows.length; i++) {\n const raw = readCell(rows[i], field, column);\n if (isBlankCell(raw)) continue;\n const n = Number(raw);\n if (isNaN(n)) continue;\n sum += n;\n count++;\n }\n return count > 0 ? sum / count : 0;\n },\n count: (rows) => rows.length,\n min: (rows, field, column) => {\n if (!rows.length) return 0;\n let min = Infinity;\n for (let i = 0; i < rows.length; i++) {\n const raw = readCell(rows[i], field, column);\n if (isBlankCell(raw)) continue;\n const v = Number(raw);\n if (isNaN(v)) continue;\n if (v < min) min = v;\n }\n return min === Infinity ? 0 : min;\n },\n max: (rows, field, column) => {\n if (!rows.length) return 0;\n let max = -Infinity;\n for (let i = 0; i < rows.length; i++) {\n const raw = readCell(rows[i], field, column);\n if (isBlankCell(raw)) continue;\n const v = Number(raw);\n if (isNaN(v)) continue;\n if (v > max) max = v;\n }\n return max === -Infinity ? 0 : max;\n },\n first: (rows, field, column) => (rows[0] ? readCell(rows[0], field, column) : undefined),\n last: (rows, field, column) => (rows.length ? readCell(rows[rows.length - 1], field, column) : undefined),\n};\n\n/** Custom aggregator registry (for plugins to add to) */\nconst customAggregators: Map<string, AggregatorFn> = new Map();\n\n/**\n * The aggregator registry singleton.\n * Plugins should access this through context or the global namespace.\n */\nexport const aggregatorRegistry = {\n /**\n * Register a custom aggregator function.\n */\n register(name: string, fn: AggregatorFn): void {\n customAggregators.set(name, fn);\n },\n\n /**\n * Unregister a custom aggregator function.\n */\n unregister(name: string): void {\n customAggregators.delete(name);\n },\n\n /**\n * Get an aggregator function by reference.\n */\n get(ref: AggregatorRef | undefined): AggregatorFn | undefined {\n if (ref === undefined) return undefined;\n if (typeof ref === 'function') return ref;\n // Check custom first, then built-in\n return customAggregators.get(ref) ?? builtInAggregators[ref];\n },\n\n /**\n * Run an aggregator on a set of rows.\n */\n run(ref: AggregatorRef | undefined, rows: any[], field: string, column?: any): any {\n const fn = this.get(ref);\n return fn ? fn(rows, field, column) : undefined;\n },\n\n /**\n * Check if an aggregator exists.\n */\n has(name: string): boolean {\n return customAggregators.has(name) || name in builtInAggregators;\n },\n\n /**\n * List all available aggregator names.\n */\n list(): string[] {\n return [...Object.keys(builtInAggregators), ...customAggregators.keys()];\n },\n};\n\n// #region Value-based Aggregators\n// Used by plugins like Pivot that work with pre-extracted numeric values\n\nexport type ValueAggregatorFn = (values: number[]) => number;\n\n/**\n * Built-in value-based aggregators.\n * These operate on arrays of numbers (unlike row-based aggregators).\n */\nconst builtInValueAggregators: Record<string, ValueAggregatorFn> = {\n sum: (vals) => {\n let sum = 0;\n for (let i = 0; i < vals.length; i++) sum += vals[i];\n return sum;\n },\n avg: (vals) => {\n if (!vals.length) return 0;\n let sum = 0;\n for (let i = 0; i < vals.length; i++) sum += vals[i];\n return sum / vals.length;\n },\n count: (vals) => vals.length,\n min: (vals) => {\n if (!vals.length) return 0;\n let min = vals[0];\n for (let i = 1; i < vals.length; i++) {\n if (vals[i] < min) min = vals[i];\n }\n return min;\n },\n max: (vals) => {\n if (!vals.length) return 0;\n let max = vals[0];\n for (let i = 1; i < vals.length; i++) {\n if (vals[i] > max) max = vals[i];\n }\n return max;\n },\n first: (vals) => vals[0] ?? 0,\n last: (vals) => vals[vals.length - 1] ?? 0,\n};\n\n/**\n * Get a value-based aggregator function.\n * Used by Pivot plugin and other features that aggregate pre-extracted values.\n *\n * @param aggFunc - Aggregation function name ('sum', 'avg', 'count', 'min', 'max', 'first', 'last')\n * @returns Aggregator function that takes number[] and returns number\n */\nexport function getValueAggregator(aggFunc: string): ValueAggregatorFn {\n return builtInValueAggregators[aggFunc] ?? builtInValueAggregators.sum;\n}\n\n/**\n * Run a value-based aggregator on a set of values.\n *\n * @param aggFunc - Aggregation function name\n * @param values - Array of numbers to aggregate\n * @returns Aggregated result\n */\nexport function runValueAggregator(aggFunc: string, values: number[]): number {\n return getValueAggregator(aggFunc)(values);\n}\n// #endregion\n","/**\n * Runtime default values for grid configuration.\n *\n * Kept out of `core/types.ts` so that file stays type-only apart from the two\n * enum-like consts whose `type` aliases are derived from them via `typeof`.\n * Import types from `./types`; import the values that back them from here.\n */\n\nimport type { A11yMessages, AnimationConfig, GridIcons } from './types';\n\n/**\n * Default English announcement messages.\n * Used when no custom messages are provided via {@link A11yConfig.messages}.\n * @since 2.0.0\n */\nexport const DEFAULT_A11Y_MESSAGES: A11yMessages = {\n sortApplied: (column, direction) => `Sorted by ${column}, ${direction}`,\n sortCleared: () => 'Sort cleared',\n filterApplied: (column) => `Filter applied on ${column}`,\n filterCleared: (column) => `Filter cleared from ${column}`,\n allFiltersCleared: () => 'All filters cleared',\n groupExpanded: (name, count) => `Group ${name} expanded, ${count} rows`,\n groupCollapsed: (name) => `Group ${name} collapsed`,\n selectionChanged: (count) => `${count} rows selected`,\n selectAllRows: () => 'Select all rows',\n selectRow: (rowIndex) => `Select row ${rowIndex + 1}`,\n columnSelected: (label) => `Column ${label} selected`,\n columnSelectionChanged: (count) => `${count} columns selected`,\n columnSelectionCleared: () => 'Column selection cleared',\n selectionAxisChanged: (toAxis) =>\n toAxis === 'column'\n ? 'Row selection cleared, column selection active'\n : 'Column selection cleared, row selection active',\n editingStarted: (rowIndex) => `Editing row ${rowIndex + 1}`,\n editingCommitted: (rowIndex) => `Row ${rowIndex + 1} saved`,\n dataLoaded: (count) => `${count} rows loaded`,\n};\n\n/**\n * Default animation configuration\n * @since 0.2.7\n */\nexport const DEFAULT_ANIMATION_CONFIG: Required<Omit<AnimationConfig, 'sort'>> = {\n mode: 'reduced-motion',\n duration: 200,\n easing: 'ease-out',\n};\n\n/**\n * Default icons used when not overridden. Most entries are short text/emoji;\n * `filter` and `filterActive` are empty strings because the actual rendering\n * is driven by the `--tbw-icon-filter[-active]-mask` CSS custom properties\n * (see `core/styles/variables.css`). Userland that wants an HTML/SVG fallback\n * via `gridConfig.icons.filter = '<svg…>'` is unaffected — that path overrides this default.\n * @since 0.1.1\n */\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: '',\n filterActive: '',\n print: '🖨️',\n};\n","/**\n * ARIA Accessibility Helpers\n *\n * Pure functions for managing ARIA attributes on grid elements.\n * Implements caching to avoid redundant DOM writes during scroll.\n *\n * @module internal/aria\n */\n\nimport { DEFAULT_A11Y_MESSAGES } from '../defaults';\nimport type { A11yMessages, GridConfig } from '../types';\n\n// #region Types\n\n/**\n * State for caching ARIA attributes to avoid redundant DOM writes.\n */\nexport interface AriaState {\n /** Last set row count */\n rowCount: number;\n /** Last set column count */\n colCount: number;\n /** Last set aria-label */\n ariaLabel: string | undefined;\n /** Last set aria-labelledby */\n ariaLabelledBy: string | undefined;\n /** Last set aria-describedby */\n ariaDescribedBy: string | undefined;\n /** Last set aria-roledescription */\n ariaRoleDescription: string | undefined;\n /** Last source row count announced via `dataLoaded`; used to suppress duplicate announcements */\n lastAnnouncedSourceCount: number;\n}\n\n/**\n * Create initial ARIA state.\n */\nexport function createAriaState(): AriaState {\n return {\n rowCount: -1,\n colCount: -1,\n ariaLabel: undefined,\n ariaLabelledBy: undefined,\n ariaDescribedBy: undefined,\n ariaRoleDescription: undefined,\n lastAnnouncedSourceCount: -1,\n };\n}\n\n// #endregion\n\n// #region Count Updates\n\n/**\n * Update ARIA row and column counts on grid elements.\n * Uses caching to avoid redundant DOM writes on every scroll frame.\n *\n * @param state - ARIA state for caching\n * @param rowsBodyEl - Element to set aria-rowcount/aria-colcount on\n * @param bodyEl - Element to set role=\"rowgroup\" on\n * @param rowCount - Current row count\n * @param colCount - Current column count\n * @returns true if anything was updated\n */\nexport function updateAriaCounts(\n state: AriaState,\n rowsBodyEl: HTMLElement | null,\n bodyEl: HTMLElement | null,\n rowCount: number,\n colCount: number,\n): boolean {\n // Skip if nothing changed (hot path optimization for scroll)\n if (rowCount === state.rowCount && colCount === state.colCount) {\n return false;\n }\n\n const prevRowCount = state.rowCount;\n state.rowCount = rowCount;\n state.colCount = colCount;\n\n // Update ARIA counts on inner grid element\n if (rowsBodyEl) {\n rowsBodyEl.setAttribute('aria-rowcount', String(rowCount));\n rowsBodyEl.setAttribute('aria-colcount', String(colCount));\n }\n\n // Set role=\"rowgroup\" on .rows only when there are rows (ARIA compliance)\n if (rowCount !== prevRowCount && bodyEl) {\n if (rowCount > 0) {\n bodyEl.setAttribute('role', 'rowgroup');\n } else {\n bodyEl.removeAttribute('role');\n }\n }\n\n return true;\n}\n\n// #endregion\n\n// #region Label Updates\n\n/**\n * Determine the effective aria-label for the grid.\n * Priority: explicit config > shell title > nothing\n *\n * The shell title is read from `config.shell.header.title`. The shell plugin\n * folds any light-DOM `<tbw-grid-header title>` into that field via its\n * `processConfig` hook (extraction #370), so core needs no plugin state here.\n *\n * @param config - Grid configuration (the merged effective config)\n * @returns The aria-label to use, or undefined\n */\nexport function getEffectiveAriaLabel<T>(config: GridConfig<T> | undefined): string | undefined {\n const explicitLabel = config?.gridAriaLabel;\n if (explicitLabel) return explicitLabel;\n\n return config?.shell?.header?.title ?? undefined;\n}\n\n/**\n * Update ARIA label and describedby attributes on the grid container.\n * Uses caching to avoid redundant DOM writes.\n *\n * @param state - ARIA state for caching\n * @param rowsBodyEl - Element to set aria-label/aria-describedby on\n * @param config - Grid configuration (the merged effective config)\n * @returns true if anything was updated\n */\nexport function updateAriaLabels<T>(\n state: AriaState,\n rowsBodyEl: HTMLElement | null,\n config: GridConfig<T> | undefined,\n): boolean {\n if (!rowsBodyEl) return false;\n\n let updated = false;\n\n // aria-labelledby wins per WAI-ARIA accessible-name precedence — when set,\n // `aria-label` is suppressed even if explicitly configured to avoid two\n // competing names being computed by AT.\n const ariaLabelledBy = config?.gridAriaLabelledBy;\n const effectiveLabel = ariaLabelledBy ? undefined : getEffectiveAriaLabel(config);\n\n // Update aria-labelledby only if changed\n if (ariaLabelledBy !== state.ariaLabelledBy) {\n state.ariaLabelledBy = ariaLabelledBy;\n if (ariaLabelledBy) {\n rowsBodyEl.setAttribute('aria-labelledby', ariaLabelledBy);\n } else {\n rowsBodyEl.removeAttribute('aria-labelledby');\n }\n updated = true;\n }\n\n // Update aria-label only if changed\n if (effectiveLabel !== state.ariaLabel) {\n state.ariaLabel = effectiveLabel;\n if (effectiveLabel) {\n rowsBodyEl.setAttribute('aria-label', effectiveLabel);\n } else {\n rowsBodyEl.removeAttribute('aria-label');\n }\n updated = true;\n }\n\n // Update aria-describedby only if changed\n const ariaDescribedBy = config?.gridAriaDescribedBy;\n if (ariaDescribedBy !== state.ariaDescribedBy) {\n state.ariaDescribedBy = ariaDescribedBy;\n if (ariaDescribedBy) {\n rowsBodyEl.setAttribute('aria-describedby', ariaDescribedBy);\n } else {\n rowsBodyEl.removeAttribute('aria-describedby');\n }\n updated = true;\n }\n\n // Update aria-roledescription only if changed. Apps can override the\n // announced role name (e.g. localized \"Tabell\" instead of generic \"grid\").\n const ariaRoleDescription = config?.gridAriaRoleDescription;\n if (ariaRoleDescription !== state.ariaRoleDescription) {\n state.ariaRoleDescription = ariaRoleDescription;\n if (ariaRoleDescription) {\n rowsBodyEl.setAttribute('aria-roledescription', ariaRoleDescription);\n } else {\n rowsBodyEl.removeAttribute('aria-roledescription');\n }\n updated = true;\n }\n\n return updated;\n}\n\n// #endregion\n\n// #region Live Announcements\n\n/**\n * Announce a message to screen readers via the grid's aria-live region.\n * Clears then sets text to ensure repeated messages are re-announced.\n *\n * Respects `effectiveConfig.a11y.announcements` — if set to `false`,\n * the announcement is silently skipped.\n *\n * @param gridEl - The grid host element (or any ancestor containing `.tbw-sr-only`)\n * @param message - The message to announce\n */\nexport function announce(gridEl: HTMLElement, message: string): void {\n // Check if announcements are disabled via config.\n // gridEl is always a DataGridElement at runtime — use property access safely.\n if (!gridEl) return;\n const config =\n 'effectiveConfig' in gridEl\n ? (gridEl as HTMLElement & { effectiveConfig?: GridConfig }).effectiveConfig\n : undefined;\n if (config?.a11y?.announcements === false) return;\n\n const el = gridEl.querySelector?.('.tbw-sr-only');\n if (!el) return;\n // Clear first so identical consecutive messages are still announced\n el.textContent = '';\n // `requestAnimationFrame` can be undefined if a debounced announcement fires\n // after the DOM/host environment is torn down (notably in unit tests). Fall\n // back to a direct assignment so `announce` never throws out of band.\n if (typeof requestAnimationFrame === 'function') {\n requestAnimationFrame(() => {\n el.textContent = message;\n });\n } else {\n el.textContent = message;\n }\n}\n\n/**\n * Get an announcement message, preferring custom overrides from config.\n * Falls back to default English messages when no override is provided.\n *\n * @param gridEl - The grid host element\n * @param key - The message key from A11yMessages\n * @param args - Arguments to pass to the message function\n */\nexport function getA11yMessage<K extends keyof A11yMessages>(\n gridEl: HTMLElement,\n key: K,\n ...args: Parameters<A11yMessages[K]>\n): string {\n const config =\n gridEl && 'effectiveConfig' in gridEl\n ? (gridEl as HTMLElement & { effectiveConfig?: GridConfig }).effectiveConfig\n : undefined;\n const customFn = config?.a11y?.messages?.[key] as ((...a: Parameters<A11yMessages[K]>) => string) | undefined;\n if (customFn) return customFn(...args);\n return (DEFAULT_A11Y_MESSAGES[key] as (...a: Parameters<A11yMessages[K]>) => string)(...args);\n}\n\n/**\n * Announce a `dataLoaded` message — but only when the source row count has\n * actually changed since the last announcement. This prevents announcement\n * spam from internal `data-change` emits triggered by sort/filter/edit, which\n * have their own dedicated announcements.\n *\n * @param gridEl - The grid host element\n * @param state - ARIA state (tracks last announced count)\n * @param sourceRowCount - Current source row count\n */\nexport function announceDataLoaded(gridEl: HTMLElement, state: AriaState, sourceRowCount: number): void {\n if (sourceRowCount === state.lastAnnouncedSourceCount) return;\n const previousAnnouncedSourceCount = state.lastAnnouncedSourceCount;\n state.lastAnnouncedSourceCount = sourceRowCount;\n // Skip only the initial sentinel → 0 transition (empty grid mount). A later\n // transition to 0 (e.g. data cleared / reloaded to an empty result set) should\n // still announce so screen-reader users hear that the grid is now empty.\n if (sourceRowCount === 0 && previousAnnouncedSourceCount < 0) return;\n announce(gridEl, getA11yMessage(gridEl, 'dataLoaded', sourceRowCount));\n}\n\n// #endregion\n","// Centralized template expression evaluation & sanitization utilities.\n// Responsible for safely interpolating {{ }} expressions while blocking\n// access to dangerous globals / reflective capabilities.\nimport type { CompiledViewFunction, EvalContext } from '../types';\n\n// #region Constants\nconst EXPR_RE = /{{\\s*([^}]+)\\s*}}/g;\nconst EMPTY_SENTINEL = '__DG_EMPTY__';\nconst SAFE_EXPR = /^[\\w$. '?+\\-*/%:()!<>=,&|]+$/;\n\n// Forbidden identifiers built at runtime to avoid static analysis scanners\n// from flagging string literals (e.g. socket.dev \"network access\" / \"uses eval\").\n// Stored as reversed, pipe-delimited tokens — reversed back when constructing the regex.\nconst _F =\n '__otorp__|__retteGenifed__|__retteSenifed__|rotcurtsnoc|wodniw|sihTlabolg|labolg|ssecorp|noitcnuF|tropmi|lave|tcelfeR|yxorP|rorrE|stnemugra|tnemucod|noitacol|eikooc|egarotSlacol|egarotSnoisses|BDdexedni|hctef|tseuqeRpttHLMX|tekcoSbeW|rekroW|rekroWderahS|rekroWecivreS|renepo|tnerap|pot|semarf|fles'\n .split('|')\n .map((s) => s.split('').reverse().join(''));\nconst FORBIDDEN = new RegExp(`__(proto|defineGetter|defineSetter)|${_F.slice(3).join('|')}|this\\\\b`);\n// #endregion\n\n// #region HTML Sanitization\n\n/**\n * Escape a plain text string for safe insertion into HTML.\n * Converts special HTML characters to their entity equivalents.\n *\n * @param text - Plain text string to escape\n * @returns HTML-safe string\n */\nexport function escapeHtml(text: string): string {\n if (!text || typeof text !== 'string') return '';\n return text\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;')\n .replace(/'/g, '&#39;');\n}\n\n/**\n * Tags that are considered dangerous and will be completely removed.\n * These can execute scripts, load external resources, or manipulate the page.\n */\nconst DANGEROUS_TAGS = new Set(\n 'script|iframe|object|embed|form|input|button|textarea|select|link|meta|base|style|template|slot|portal|frame|frameset|applet|noscript|noembed|plaintext|xmp|listing|math'.split(\n '|',\n ),\n);\n\n/**\n * Attributes that are considered dangerous - event handlers and data loading.\n */\nconst DANGEROUS_ATTR_PATTERN = /^on\\w+$/i;\n\n/**\n * Attributes that can contain URLs which might be javascript: or data: URIs.\n */\nconst URL_ATTRS = new Set('href|src|action|formaction|data|srcdoc|xlink:href|poster|srcset'.split('|'));\n\n/**\n * Protocol patterns that are dangerous in URLs.\n */\nconst DANGEROUS_URL_PROTOCOL = /^\\s*(javascript|vbscript|data|blob):/i;\n\n/**\n * Sanitize an HTML string by removing dangerous tags and attributes.\n *\n * Prefer {@link setSanitizedHTML} wherever the result is destined for an\n * element: this function has to re-serialize the sanitized tree back to a\n * string, and the caller's `innerHTML =` then re-parses it. That\n * parse→serialize→parse round trip is the classic mutation-XSS vector, and it\n * costs a second parse. Use this overload only when a string is genuinely\n * required (e.g. the value is stored, compared, or handed to another API).\n *\n * @param html - Raw HTML string to sanitize\n * @returns Sanitized HTML string safe for innerHTML\n */\nexport function sanitizeHTML(html: string): string {\n if (!html || typeof html !== 'string') return '';\n\n // Fast path: if no HTML tags at all, return as-is (already safe)\n if (html.indexOf('<') === -1) return html;\n\n const template = document.createElement('template');\n template.innerHTML = html;\n\n sanitizeNode(template.content);\n\n return template.innerHTML;\n}\n\n/**\n * Parse and sanitize an HTML string into a detached {@link DocumentFragment}.\n *\n * This is the primitive {@link setSanitizedHTML} builds on, exposed for callers\n * that need to inspect or relocate the nodes before insertion.\n *\n * @param html - Raw HTML string to sanitize\n * @returns Fragment containing the sanitized nodes (empty for falsy input)\n */\nexport function sanitizeToFragment(html: string): DocumentFragment {\n const template = document.createElement('template');\n if (html && typeof html === 'string') {\n template.innerHTML = html;\n sanitizeNode(template.content);\n }\n return template.content;\n}\n\n/**\n * Replace an element's children with the sanitized parse of `html`.\n *\n * **This is the canonical sink for user-supplied markup** — cell/header/group\n * renderer string returns, `gridConfig.icons.*`, light-DOM tool panel fallback\n * content, and any new extension point that turns a user string into markup.\n * `textContent` is wrong here (it renders SVG icon strings as literal text) and\n * raw `innerHTML` is wrong (it is an XSS sink).\n *\n * Nodes are moved from the parse fragment rather than re-serialized, so the\n * sanitized tree is exactly what lands in the document.\n *\n * @param el - Element whose children are replaced\n * @param html - Raw HTML string to sanitize and insert\n */\nexport function setSanitizedHTML(el: Element, html: string): void {\n if (!html || typeof html !== 'string') {\n el.replaceChildren();\n return;\n }\n // Nothing for the parser to do: no markup and no entities to decode.\n if (html.indexOf('<') === -1 && html.indexOf('&') === -1) {\n el.textContent = html;\n return;\n }\n el.replaceChildren(sanitizeToFragment(html));\n}\n\n/**\n * Recursively sanitize a DOM node tree.\n */\nfunction sanitizeNode(root: DocumentFragment | Element): void {\n const toRemove: Element[] = [];\n\n // Use querySelectorAll to find all elements, then filter\n const elements = root.querySelectorAll('*');\n\n for (const el of elements) {\n const tagName = el.tagName.toLowerCase();\n\n // Check if tag is dangerous\n if (DANGEROUS_TAGS.has(tagName)) {\n toRemove.push(el);\n continue;\n }\n\n // SVG elements need special handling - they can contain script-like behavior\n if (tagName === 'svg' || el.namespaceURI === 'http://www.w3.org/2000/svg') {\n // Remove entire SVG if it has any suspicious attributes\n const hasDangerousContent = Array.from(el.attributes).some(\n (attr) => DANGEROUS_ATTR_PATTERN.test(attr.name) || attr.name === 'href' || attr.name === 'xlink:href',\n );\n if (hasDangerousContent) {\n toRemove.push(el);\n continue;\n }\n }\n\n // Check and remove dangerous attributes\n const attrsToRemove: string[] = [];\n for (const attr of el.attributes) {\n const attrName = attr.name.toLowerCase();\n\n // Event handlers (onclick, onerror, onload, etc.)\n if (DANGEROUS_ATTR_PATTERN.test(attrName)) {\n attrsToRemove.push(attr.name);\n continue;\n }\n\n // `is=` upgrades a plain tag into a registered customized built-in, so it\n // can smuggle behaviour past the tag-name check above.\n if (attrName === 'is') {\n attrsToRemove.push(attr.name);\n continue;\n }\n\n // URL attributes with dangerous protocols\n if (URL_ATTRS.has(attrName) && DANGEROUS_URL_PROTOCOL.test(attr.value)) {\n attrsToRemove.push(attr.name);\n continue;\n }\n\n // style attribute can contain expressions (IE) or url() with javascript:\n if (attrName === 'style' && /expression\\s*\\(|javascript:|behavior\\s*:/i.test(attr.value)) {\n attrsToRemove.push(attr.name);\n continue;\n }\n }\n\n attrsToRemove.forEach((name) => el.removeAttribute(name));\n }\n\n // Remove dangerous elements (do this after iteration to avoid modifying during traversal)\n toRemove.forEach((el) => el.remove());\n}\n\n// #endregion\n\n// #region Template Evaluation\n\n// #region Safe Expression Evaluator\n// A minimal recursive-descent evaluator for the token set allowed by SAFE_EXPR.\n// Replaces `new Function()` to avoid dynamic code execution warnings from\n// static analysis tools (socket.dev, npm audit) while maintaining identical\n// functionality for expressions that pass the allowlist guards.\n//\n// Supported grammar (matching SAFE_EXPR = /^[\\w$. '?+\\-*/%:()!<>=,&|]+$/):\n// ternary → logicalOr ('?' ternary ':' ternary)?\n// logicalOr → logicalAnd ('||' logicalAnd)*\n// logicalAnd → equality ('&&' equality)*\n// equality → comparison (('==' | '!=' | '===' | '!==') comparison)*\n// comparison → additive (('<' | '>' | '<=' | '>=') additive)*\n// additive → multiplicative (('+' | '-') multiplicative)*\n// multiplicative → unary (('*' | '/' | '%') unary)*\n// unary → '!' unary | primary\n// primary → NUMBER | STRING | IDENT ('.' IDENT)? | '(' ternary ')'\n\ntype Token =\n { type: 'num'; v: number } | { type: 'str'; v: string } | { type: 'id'; v: string } | { type: 'op'; v: string };\n\nfunction tokenize(expr: string): Token[] | null {\n const tokens: Token[] = [];\n let i = 0;\n while (i < expr.length) {\n const ch = expr[i];\n if (ch === ' ') {\n i++;\n continue;\n }\n\n // Numbers\n if ((ch >= '0' && ch <= '9') || (ch === '.' && i + 1 < expr.length && expr[i + 1] >= '0' && expr[i + 1] <= '9')) {\n const start = i;\n while (i < expr.length && ((expr[i] >= '0' && expr[i] <= '9') || expr[i] === '.')) i++;\n tokens.push({ type: 'num', v: Number(expr.slice(start, i)) });\n continue;\n }\n\n // String literals (single-quoted only, per SAFE_EXPR)\n if (ch === \"'\") {\n let s = '';\n i++; // skip opening quote\n while (i < expr.length && expr[i] !== \"'\") s += expr[i++];\n if (i >= expr.length) return null; // unterminated\n i++; // skip closing quote\n tokens.push({ type: 'str', v: s });\n continue;\n }\n\n // Identifiers (value, row, row.field)\n if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || ch === '_' || ch === '$') {\n const start = i;\n while (i < expr.length && /[\\w$.]/.test(expr[i])) i++;\n tokens.push({ type: 'id', v: expr.slice(start, i) });\n continue;\n }\n\n // Multi-char operators\n if (i + 2 < expr.length) {\n const tri = expr.slice(i, i + 3);\n if (tri === '===' || tri === '!==') {\n tokens.push({ type: 'op', v: tri });\n i += 3;\n continue;\n }\n }\n if (i + 1 < expr.length) {\n const bi = expr.slice(i, i + 2);\n if (bi === '==' || bi === '!=' || bi === '<=' || bi === '>=' || bi === '&&' || bi === '||') {\n tokens.push({ type: 'op', v: bi });\n i += 2;\n continue;\n }\n }\n\n // Single-char operators\n if ('+-*/%<>!?:(),'.includes(ch)) {\n tokens.push({ type: 'op', v: ch });\n i++;\n continue;\n }\n\n return null; // unexpected character\n }\n return tokens;\n}\n\nfunction safeEval(expr: string, ctx: EvalContext): unknown {\n const toks = tokenize(expr);\n if (!toks) return undefined;\n const tokens: Token[] = toks;\n let pos = 0;\n\n function peek(): Token | undefined {\n return tokens[pos];\n }\n function advance(): Token {\n return tokens[pos++];\n }\n function match(type: string, val?: string): boolean {\n const t = peek();\n if (!t) return false;\n if (t.type !== type) return false;\n if (val !== undefined && t.v !== val) return false;\n return true;\n }\n\n function resolveId(name: string): unknown {\n if (name === 'value') return ctx.value;\n if (name.startsWith('row.')) {\n const key = name.slice(4);\n return ctx.row ? ctx.row[key] : undefined;\n }\n if (name.startsWith('typeDefault.')) {\n const key = name.slice('typeDefault.'.length);\n return ctx.typeDefault ? ctx.typeDefault[key] : undefined;\n }\n // bare 'row' reference\n if (name === 'row') return ctx.row;\n if (name === 'typeDefault') return ctx.typeDefault;\n return undefined;\n }\n\n function parseTernary(): unknown {\n const result = parseLogicalOr();\n if (match('op', '?')) {\n advance(); // consume '?'\n const consequent = parseTernary();\n if (!match('op', ':')) return undefined;\n advance(); // consume ':'\n const alternate = parseTernary();\n return result ? consequent : alternate;\n }\n return result;\n }\n\n function parseLogicalOr(): unknown {\n let left = parseLogicalAnd();\n while (match('op', '||')) {\n advance();\n left = left || parseLogicalAnd();\n }\n return left;\n }\n\n function parseLogicalAnd(): unknown {\n let left = parseEquality();\n while (match('op', '&&')) {\n advance();\n left = left && parseEquality();\n }\n return left;\n }\n\n function parseEquality(): unknown {\n let left = parseComparison();\n while (true) {\n const t = peek();\n if (!t || t.type !== 'op') break;\n if (t.v === '===') {\n advance();\n left = left === parseComparison();\n } else if (t.v === '!==') {\n advance();\n left = left !== parseComparison();\n } else if (t.v === '==') {\n advance();\n left = left == parseComparison();\n } else if (t.v === '!=') {\n advance();\n left = left != parseComparison();\n } else break;\n }\n return left;\n }\n\n function parseComparison(): unknown {\n let left = parseAdditive();\n while (true) {\n const t = peek();\n if (!t || t.type !== 'op') break;\n if (t.v === '<') {\n advance();\n left = (left as number) < (parseAdditive() as number);\n } else if (t.v === '>') {\n advance();\n left = (left as number) > (parseAdditive() as number);\n } else if (t.v === '<=') {\n advance();\n left = (left as number) <= (parseAdditive() as number);\n } else if (t.v === '>=') {\n advance();\n left = (left as number) >= (parseAdditive() as number);\n } else break;\n }\n return left;\n }\n\n function parseAdditive(): unknown {\n let left = parseMultiplicative();\n while (true) {\n const t = peek();\n if (!t || t.type !== 'op') break;\n if (t.v === '+') {\n advance();\n left = (left as number) + (parseMultiplicative() as number);\n } else if (t.v === '-') {\n advance();\n left = (left as number) - (parseMultiplicative() as number);\n } else break;\n }\n return left;\n }\n\n function parseMultiplicative(): unknown {\n let left = parseUnary();\n while (true) {\n const t = peek();\n if (!t || t.type !== 'op') break;\n if (t.v === '*') {\n advance();\n left = (left as number) * (parseUnary() as number);\n } else if (t.v === '/') {\n advance();\n left = (left as number) / (parseUnary() as number);\n } else if (t.v === '%') {\n advance();\n left = (left as number) % (parseUnary() as number);\n } else break;\n }\n return left;\n }\n\n function parseUnary(): unknown {\n if (match('op', '!')) {\n advance();\n return !parseUnary();\n }\n return parsePrimary();\n }\n\n function parsePrimary(): unknown {\n const t = peek();\n if (!t) return undefined;\n if (t.type === 'num') {\n advance();\n return t.v;\n }\n if (t.type === 'str') {\n advance();\n return t.v;\n }\n if (t.type === 'id') {\n advance();\n return resolveId(t.v as string);\n }\n if (t.type === 'op' && t.v === '(') {\n advance(); // consume '('\n const result = parseTernary();\n if (match('op', ')')) advance(); // consume ')'\n return result;\n }\n // Unary minus for negative numbers\n if (t.type === 'op' && t.v === '-') {\n advance();\n return -(parsePrimary() as number);\n }\n return undefined;\n }\n\n const result = parseTernary();\n // Ensure all tokens were consumed\n if (pos < tokens.length) return undefined;\n return result;\n}\n// #endregion\n\nexport function evalTemplateString(raw: string, ctx: EvalContext): string {\n if (!raw || raw.indexOf('{{') === -1) return raw; // fast path (no expressions)\n const parts: { expr: string; result: string }[] = [];\n const evaluated = raw.replace(EXPR_RE, (_m, expr) => {\n const res = evalSingle(expr, ctx);\n parts.push({ expr: expr.trim(), result: res });\n // The template markup is author-controlled; the interpolated data is not.\n // Escaping here stops row data from injecting elements or breaking out of\n // an attribute (sanitizeHTML downstream only strips *dangerous* markup).\n return res === EMPTY_SENTINEL ? res : escapeHtml(res);\n });\n const finalStr = postProcess(evaluated);\n // If every part evaluated to EMPTY_SENTINEL we treat this as intentionally blank.\n // If any expression was blocked due to forbidden token (EMPTY_SENTINEL) we *still* only output ''\n // but do not escalate to BLOCKED_SENTINEL unless the original contained explicit forbidden tokens.\n const allEmpty = parts.length && parts.every((p) => p.result === '' || p.result === EMPTY_SENTINEL);\n const hadForbidden = REFLECTIVE_RE.test(raw);\n if (hadForbidden || allEmpty) return '';\n return finalStr;\n}\n\nfunction evalSingle(expr: string, ctx: EvalContext): string {\n expr = (expr || '').trim();\n if (!expr) return EMPTY_SENTINEL;\n if (REFLECTIVE_RE.test(expr)) return EMPTY_SENTINEL;\n if (expr === 'value') return ctx.value == null ? EMPTY_SENTINEL : String(ctx.value);\n if (expr.startsWith('row.') && !/[()?]/.test(expr) && !expr.includes(':')) {\n const key = expr.slice(4);\n const v = ctx.row ? ctx.row[key] : undefined;\n return v == null ? EMPTY_SENTINEL : String(v);\n }\n if (expr.startsWith('typeDefault.') && !/[()?]/.test(expr) && !expr.includes(':')) {\n const key = expr.slice('typeDefault.'.length);\n const v = ctx.typeDefault ? ctx.typeDefault[key] : undefined;\n return v == null ? EMPTY_SENTINEL : String(v);\n }\n if (expr.length > 80) return EMPTY_SENTINEL;\n if (!SAFE_EXPR.test(expr) || FORBIDDEN.test(expr)) return EMPTY_SENTINEL;\n const dotChain = expr.match(/\\./g);\n if (dotChain && dotChain.length > 1) return EMPTY_SENTINEL;\n try {\n const out = safeEval(expr, ctx);\n const str = out == null ? '' : String(out);\n if (REFLECTIVE_RE.test(str)) return EMPTY_SENTINEL;\n return str || EMPTY_SENTINEL;\n } catch {\n return EMPTY_SENTINEL;\n }\n}\n// #endregion\n\n// #region Cell Scrubbing\n/** Pattern matching reflective/introspective APIs that must be stripped from output. */\nconst REFLECTIVE_RE = /Reflect|Proxy|ownKeys/;\n\nfunction postProcess(s: string): string {\n if (!s) return s;\n return s.replace(new RegExp(EMPTY_SENTINEL, 'g'), '').replace(/Reflect\\.[^<>{}\\s]+|\\bProxy\\b|ownKeys\\([^)]*\\)/g, '');\n}\n\nexport function finalCellScrub(cell: HTMLElement): void {\n if (!REFLECTIVE_RE.test(cell.textContent || '')) return;\n // First pass: clear only text nodes containing forbidden tokens\n for (const n of cell.childNodes) {\n if (n.nodeType === Node.TEXT_NODE && REFLECTIVE_RE.test(n.textContent || '')) n.textContent = '';\n }\n // If forbidden tokens persist in element nodes, nuke everything\n if (REFLECTIVE_RE.test(cell.textContent || '')) {\n cell.textContent = '';\n }\n}\n// #endregion\n\n// #region Template Compilation\nexport function compileTemplate(raw: string) {\n if (REFLECTIVE_RE.test(raw)) {\n const fn = (() => '') as CompiledViewFunction;\n fn.__blocked = true;\n return fn;\n }\n const fn = ((ctx: EvalContext) => evalTemplateString(raw, ctx)) as CompiledViewFunction;\n fn.__blocked = false;\n return fn;\n}\n// #endregion\n","/**\n * Loading State Module\n *\n * Handles loading overlays, row loading, and cell loading states.\n * Provides DOM manipulation helpers for loading indicators.\n *\n * @module internal/loading\n */\n\nimport type { GridConfig, LoadingContext } from '../types';\nimport { setSanitizedHTML } from './sanitize';\n\n/**\n * Create the default spinner element.\n * @param size - 'large' for grid overlay, 'small' for row/cell\n */\nexport function createDefaultSpinner(size: 'large' | 'small'): HTMLElement {\n const spinner = document.createElement('div');\n spinner.className = `tbw-spinner tbw-spinner--${size}`;\n spinner.setAttribute('role', 'progressbar');\n spinner.setAttribute('aria-label', 'Loading');\n return spinner;\n}\n\n/**\n * Create loading content using custom renderer or default spinner.\n *\n * Strings returned by a custom renderer are passed through `setSanitizedHTML()`\n * — mirroring the cell, header and empty-state render paths — so a message\n * interpolating server data (e.g. `Loading ${dataset.name}…`) is not an XSS\n * sink.\n *\n * @param size - 'large' for grid overlay, 'small' for row/cell\n * @param renderer - Optional custom loading renderer from config\n */\nexport function createLoadingContent(size: 'large' | 'small', renderer?: GridConfig['loadingRenderer']): HTMLElement {\n if (renderer) {\n const context: LoadingContext = { size };\n const result = renderer(context);\n if (typeof result === 'string') {\n const wrapper = document.createElement('div');\n setSanitizedHTML(wrapper, result);\n return wrapper;\n }\n return result;\n }\n\n return createDefaultSpinner(size);\n}\n\n/**\n * Create or update the loading overlay element.\n * @param renderer - Optional custom loading renderer from config\n */\nexport function createLoadingOverlay(renderer?: GridConfig['loadingRenderer']): HTMLElement {\n const overlay = document.createElement('div');\n overlay.className = 'tbw-loading-overlay';\n overlay.setAttribute('role', 'status');\n overlay.setAttribute('aria-live', 'polite');\n overlay.appendChild(createLoadingContent('large', renderer));\n return overlay;\n}\n\n/**\n * Show the loading overlay on the grid root element.\n * @param gridRoot - The .tbw-grid-root element\n * @param overlayEl - The overlay element (will be cached by caller)\n */\nexport function showLoadingOverlay(gridRoot: Element, overlayEl: HTMLElement): void {\n gridRoot.appendChild(overlayEl);\n}\n\n/**\n * Hide the loading overlay.\n * @param overlayEl - The overlay element to remove\n */\nexport function hideLoadingOverlay(overlayEl: HTMLElement | undefined): void {\n overlayEl?.remove();\n}\n\n/**\n * Update a row element's loading state.\n * Uses real DOM elements instead of ::before/::after pseudo-elements\n * because the selection plugin already uses ::after on rows for border styling.\n * @param rowEl - The row element\n * @param loading - Whether the row is loading\n */\nexport function setRowLoadingState(rowEl: HTMLElement, loading: boolean): void {\n if (loading) {\n rowEl.classList.add('tbw-row-loading');\n rowEl.setAttribute('aria-busy', 'true');\n\n // Create overlay + spinner DOM elements if not already present\n if (!rowEl.querySelector('.tbw-row-loading-overlay')) {\n const overlay = document.createElement('div');\n overlay.className = 'tbw-row-loading-overlay';\n overlay.setAttribute('aria-hidden', 'true');\n\n const spinner = document.createElement('div');\n spinner.className = 'tbw-row-loading-spinner';\n overlay.appendChild(spinner);\n\n rowEl.appendChild(overlay);\n }\n } else {\n rowEl.classList.remove('tbw-row-loading');\n rowEl.removeAttribute('aria-busy');\n\n // Remove overlay + spinner DOM elements\n rowEl.querySelector('.tbw-row-loading-overlay')?.remove();\n }\n}\n\n/**\n * Update a cell element's loading state.\n * @param cellEl - The cell element\n * @param loading - Whether the cell is loading\n */\nexport function setCellLoadingState(cellEl: HTMLElement, loading: boolean): void {\n if (loading) {\n cellEl.classList.add('tbw-cell-loading');\n cellEl.setAttribute('aria-busy', 'true');\n } else {\n cellEl.classList.remove('tbw-cell-loading');\n cellEl.removeAttribute('aria-busy');\n }\n}\n","/**\n * Centralized diagnostic messages for @toolbox-web/grid.\n *\n * Every user-facing warning, error, or info message in the grid has a unique\n * diagnostic code (e.g. `TBW001`). Each code maps to a section on the online\n * troubleshooting page, giving developers a direct link to resolution steps.\n *\n * ## Usage\n *\n * ```ts\n * import { MISSING_BREAKPOINT, warnDiagnostic, throwDiagnostic } from './diagnostics';\n *\n * // Warn with a code\n * warnDiagnostic(MISSING_BREAKPOINT, 'Set a breakpoint...', gridId);\n *\n * // Throw with a code\n * throwDiagnostic(MISSING_ROW_ID, 'Configure getRowId...', gridId);\n * ```\n *\n * Plugins should prefer `this.warn(MISSING_BREAKPOINT, message)` via BaseGridPlugin\n * instead of importing this module directly.\n *\n * @internal\n */\n\n// #region Grid Prefix\n\n/**\n * Build the `[tbw-grid]` or `[tbw-grid#my-id]` log prefix.\n * Pass `pluginName` for a scoped prefix like `[tbw-grid:SelectionPlugin]`.\n */\nexport function gridPrefix(gridId?: string, pluginName?: string): string {\n const id = gridId ? `#${gridId}` : '';\n const plugin = pluginName ? `:${pluginName}` : '';\n return `[tbw-grid${id}${plugin}]`;\n}\n\n// #endregion\n\n// #region Diagnostic Codes\n\n/**\n * Diagnostic codes used across the grid library.\n *\n * Each code is an individual export so that unused codes are tree-shaken\n * from bundles that don't reference them (esbuild can't tree-shake\n * properties from a single object).\n *\n * Naming: TBW + 3-digit number.\n * Ranges:\n * 001–019 Configuration validation (missing plugins, bad config)\n * 020–029 Plugin lifecycle (dependencies, incompatibilities, alias collapse)\n * 030–039 Feature registry\n * 040–049 Row operations (row ID, row mutations)\n * 050–059 Column operations (width, template)\n * 060–069 Rendering (callbacks, formatters, external views)\n * 070–079 Shell (tool panels, header/toolbar content)\n * 080–089 Editing & editors\n * 090–099 Print\n * 100–109 Clipboard\n * 110–119 Plugin-specific (responsive, undo-redo, grouping-columns)\n * 120–129 Style injection\n * 130–139 Attribute parsing\n * 140–149 DataSource / ServerSide\n */\n\n// --- Config validation (001–019) ---\n/** Column uses a plugin-owned property but the plugin is not loaded. */\nexport const MISSING_PLUGIN = 'TBW001' as const;\n/** Grid config uses a plugin-owned property but the plugin is not loaded. */\nexport const MISSING_PLUGIN_CONFIG = 'TBW002' as const;\n/** Plugin config rule violation (error severity). */\nexport const CONFIG_RULE_ERROR = 'TBW003' as const;\n/** Plugin config rule violation (warning severity). */\nexport const CONFIG_RULE_WARN = 'TBW004' as const;\n\n// --- Plugin lifecycle (020–029) ---\n/** Required plugin dependency is missing. */\nexport const MISSING_DEPENDENCY = 'TBW020' as const;\n/** Optional plugin dependency is missing. */\nexport const OPTIONAL_DEPENDENCY = 'TBW021' as const;\n/** Two loaded plugins are incompatible. */\nexport const INCOMPATIBLE_PLUGINS = 'TBW022' as const;\n/** Two plugin instances resolve to the same canonical name (alias collapse). */\nexport const PLUGIN_ALIAS_COLLAPSE = 'TBW023' as const;\n/** Error thrown inside a plugin event handler. */\nexport const PLUGIN_EVENT_ERROR = 'TBW024' as const;\n/** Conflicting config values found while merging alias-collapsed plugin instances. */\nexport const PLUGIN_ALIAS_CONFIG_CONFLICT = 'TBW025' as const;\n\n// --- Feature registry (030–039) ---\n/** Feature was re-registered (overwritten). */\nexport const FEATURE_REREGISTERED = 'TBW030' as const;\n/** Feature configured but not imported. */\nexport const FEATURE_NOT_IMPORTED = 'TBW031' as const;\n/** Feature depends on another feature that is not enabled. */\nexport const FEATURE_MISSING_DEP = 'TBW032' as const;\n\n// --- Row operations (040–049) ---\n/** Cannot determine row ID (no getRowId and no id property). */\nexport const MISSING_ROW_ID = 'TBW040' as const;\n/** Row with given ID not found. */\nexport const ROW_NOT_FOUND = 'TBW041' as const;\n\n// --- Column operations (050–059) ---\n/** Column has an invalid CSS width value. */\nexport const INVALID_COLUMN_WIDTH = 'TBW050' as const;\n\n// --- Rendering callbacks (060–069) ---\n/** rowClass callback threw an error. */\nexport const ROW_CLASS_ERROR = 'TBW060' as const;\n/** cellClass callback threw an error. */\nexport const CELL_CLASS_ERROR = 'TBW061' as const;\n/** Column format function threw an error. */\nexport const FORMAT_ERROR = 'TBW062' as const;\n/** External view mount() threw an error. */\nexport const VIEW_MOUNT_ERROR = 'TBW063' as const;\n/** External view event dispatch error. */\nexport const VIEW_DISPATCH_ERROR = 'TBW064' as const;\n\n// --- Shell (070–079) ---\n/** Tool panel missing required id. */\nexport const TOOL_PANEL_MISSING_ATTR = 'TBW070' as const;\n/** No tool panels registered. */\nexport const NO_TOOL_PANELS = 'TBW071' as const;\n/** Tool panel section not found. */\nexport const TOOL_PANEL_NOT_FOUND = 'TBW072' as const;\n/** Tool panel already registered. */\nexport const TOOL_PANEL_DUPLICATE = 'TBW073' as const;\n/** Header content already registered. */\nexport const HEADER_CONTENT_DUPLICATE = 'TBW074' as const;\n/** Toolbar content already registered. */\nexport const TOOLBAR_CONTENT_DUPLICATE = 'TBW075' as const;\n\n// --- Editing & editors (080–089) ---\n/** External editor mount() threw an error. */\nexport const EDITOR_MOUNT_ERROR = 'TBW080' as const;\n\n// --- Print (090–099) ---\n/** Print already in progress. */\nexport const PRINT_IN_PROGRESS = 'TBW090' as const;\n/** Grid not available for printing. */\nexport const PRINT_NO_GRID = 'TBW091' as const;\n/** Print operation failed. */\nexport const PRINT_FAILED = 'TBW092' as const;\n/** Multiple elements share the same grid ID (print isolation issue). */\nexport const PRINT_DUPLICATE_ID = 'TBW093' as const;\n\n// --- Clipboard (100–109) ---\n/** Clipboard API write failed. */\nexport const CLIPBOARD_FAILED = 'TBW100' as const;\n\n// --- Plugin-specific (110–119) ---\n/** ResponsivePlugin: no breakpoint configured. */\nexport const MISSING_BREAKPOINT = 'TBW110' as const;\n/** UndoRedoPlugin: transaction already in progress. */\nexport const TRANSACTION_IN_PROGRESS = 'TBW111' as const;\n/** UndoRedoPlugin: no transaction in progress. */\nexport const NO_TRANSACTION = 'TBW112' as const;\n/** GroupingColumnsPlugin: missing id or header on column group definition. */\nexport const COLUMN_GROUP_NO_ID = 'TBW113' as const;\n/** GroupingColumnsPlugin: conflicting columnGroups sources. */\nexport const COLUMN_GROUPS_CONFLICT = 'TBW114' as const;\n\n// --- Style injection (120–129) ---\n/** Failed to extract grid.css from document stylesheets. */\nexport const STYLE_EXTRACT_FAILED = 'TBW120' as const;\n/** Could not find grid.css in document.styleSheets. */\nexport const STYLE_NOT_FOUND = 'TBW121' as const;\n\n// --- Attribute parsing (130–139) ---\n/** Invalid JSON in an HTML attribute. */\nexport const INVALID_ATTRIBUTE_JSON = 'TBW130' as const;\n\n// --- DataSource / ServerSide (140–149) ---\n/** ServerSidePlugin: getRows() rejected. */\nexport const DATASOURCE_FETCH_ERROR = 'TBW140' as const;\n/** ServerSidePlugin: getChildRows() rejected. */\nexport const DATASOURCE_CHILD_FETCH_ERROR = 'TBW141' as const;\n/** ServerSidePlugin: getChildRows() not implemented but a plugin queried for children. */\nexport const DATASOURCE_NO_CHILD_HANDLER = 'TBW142' as const;\n/** ServerSidePlugin: concurrent request limit reached, request deferred. */\nexport const DATASOURCE_THROTTLED = 'TBW143' as const;\n\nexport type DiagnosticCode =\n | typeof MISSING_PLUGIN\n | typeof MISSING_PLUGIN_CONFIG\n | typeof CONFIG_RULE_ERROR\n | typeof CONFIG_RULE_WARN\n | typeof MISSING_DEPENDENCY\n | typeof OPTIONAL_DEPENDENCY\n | typeof INCOMPATIBLE_PLUGINS\n | typeof PLUGIN_ALIAS_COLLAPSE\n | typeof PLUGIN_EVENT_ERROR\n | typeof PLUGIN_ALIAS_CONFIG_CONFLICT\n | typeof FEATURE_REREGISTERED\n | typeof FEATURE_NOT_IMPORTED\n | typeof FEATURE_MISSING_DEP\n | typeof MISSING_ROW_ID\n | typeof ROW_NOT_FOUND\n | typeof INVALID_COLUMN_WIDTH\n | typeof ROW_CLASS_ERROR\n | typeof CELL_CLASS_ERROR\n | typeof FORMAT_ERROR\n | typeof VIEW_MOUNT_ERROR\n | typeof VIEW_DISPATCH_ERROR\n | typeof TOOL_PANEL_MISSING_ATTR\n | typeof NO_TOOL_PANELS\n | typeof TOOL_PANEL_NOT_FOUND\n | typeof TOOL_PANEL_DUPLICATE\n | typeof HEADER_CONTENT_DUPLICATE\n | typeof TOOLBAR_CONTENT_DUPLICATE\n | typeof EDITOR_MOUNT_ERROR\n | typeof PRINT_IN_PROGRESS\n | typeof PRINT_NO_GRID\n | typeof PRINT_FAILED\n | typeof PRINT_DUPLICATE_ID\n | typeof CLIPBOARD_FAILED\n | typeof MISSING_BREAKPOINT\n | typeof TRANSACTION_IN_PROGRESS\n | typeof NO_TRANSACTION\n | typeof COLUMN_GROUP_NO_ID\n | typeof COLUMN_GROUPS_CONFLICT\n | typeof STYLE_EXTRACT_FAILED\n | typeof STYLE_NOT_FOUND\n | typeof INVALID_ATTRIBUTE_JSON\n | typeof DATASOURCE_FETCH_ERROR\n | typeof DATASOURCE_CHILD_FETCH_ERROR\n | typeof DATASOURCE_NO_CHILD_HANDLER\n | typeof DATASOURCE_THROTTLED;\n\n// #endregion\n\n// #region Docs URL\n\nconst DOCS_BASE = 'https://toolboxjs.com/grid/errors';\n\n/** Build a direct link to the troubleshooting section for a code. */\nfunction docsUrl(code: DiagnosticCode): string {\n return `${DOCS_BASE}#${code.toLowerCase()}`;\n}\n\n// #endregion\n\n// #region Formatting\n\n/**\n * Format a diagnostic message with prefix, code, and docs link.\n *\n * Output format:\n * ```\n * [tbw-grid#my-id] TBW001: Your message here.\n *\n * → More info: https://toolboxjs.com/grid/errors#tbw001\n * ```\n */\nexport function formatDiagnostic(code: DiagnosticCode, message: string, gridId?: string, pluginName?: string): string {\n const prefix = gridPrefix(gridId, pluginName);\n return `${prefix} ${code}: ${message}\\n\\n → More info: ${docsUrl(code)}`;\n}\n\n// #endregion\n\n// #region Public API\n\n/**\n * Throw an error with a diagnostic code and docs link.\n * Use for configuration errors and API misuse that should halt execution.\n */\nexport function throwDiagnostic(code: DiagnosticCode, message: string, gridId?: string, pluginName?: string): never {\n throw new Error(formatDiagnostic(code, message, gridId, pluginName));\n}\n\n/**\n * Log a warning with a diagnostic code and docs link.\n * Use for recoverable issues the developer should fix.\n */\nexport function warnDiagnostic(code: DiagnosticCode, message: string, gridId?: string, pluginName?: string): void {\n console.warn(formatDiagnostic(code, message, gridId, pluginName));\n}\n\n/**\n * Log a debug message with a diagnostic code and docs link.\n * Use for optional/soft dependency notifications — visible only when\n * the browser DevTools \"Verbose\" log level is enabled.\n */\nexport function debugDiagnostic(code: DiagnosticCode, message: string, gridId?: string, pluginName?: string): void {\n console.debug(formatDiagnostic(code, message, gridId, pluginName));\n}\n\n/**\n * Log an error with a diagnostic code and docs link.\n * Use for non-throwing errors (e.g., failed async operations).\n */\nexport function errorDiagnostic(code: DiagnosticCode, message: string, gridId?: string, pluginName?: string): void {\n console.error(formatDiagnostic(code, message, gridId, pluginName));\n}\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 { HeaderContentDefinition, ToolPanelDefinition } from '../../plugins/shell/types';\nimport { DEFAULT_GRID_ICONS } from '../defaults';\nimport {\n type DiagnosticCode,\n PLUGIN_ALIAS_CONFIG_CONFLICT,\n formatDiagnostic,\n gridPrefix,\n} from '../internal/diagnostics';\nimport { setSanitizedHTML } from '../internal/sanitize';\nimport type {\n ColumnConfig,\n ColumnState,\n GridConfig,\n GridIcons,\n GridPlugin,\n IconValue,\n PluginNameMap,\n Translate,\n} from '../types';\n\n// Re-export shared plugin types for convenience\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 GridPlugin>(PluginClass: new (...args: any[]) => T): T | undefined;\n getPluginByName<K extends string>(\n name: K,\n ): (K extends keyof PluginNameMap ? PluginNameMap[K] : 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 // Focus Management\n /** Register an external DOM element as a logical focus container. @internal Plugin API */\n registerExternalFocusContainer?(el: Element): void;\n /** Unregister a previously registered external focus container. @internal Plugin API */\n unregisterExternalFocusContainer?(el: Element): void;\n /** Check whether focus is logically inside this grid (own DOM + external containers). @internal Plugin API */\n containsFocus?(node?: Node | null): boolean;\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 * @since 0.4.1\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. Silent if missing\n * (set {@link severity} to `'warn'` or `'info'` to opt into a message).\n * @default true\n */\n required?: boolean;\n\n /**\n * Explicit severity when this dependency is missing. Overrides the default\n * derived from {@link required}.\n * - `'error'`: throw (halts grid setup). Default when `required` is not `false`.\n * - `'warn'`: log a console warning and continue (development only).\n * - `'info'`: log a verbose console.debug message and continue (development only).\n *\n * When omitted, a missing hard dependency throws and a missing soft\n * dependency is silent — preserving backward-compatible behavior.\n * @since 2.16.0\n */\n severity?: 'error' | 'warn' | 'info';\n\n /**\n * Optional predicate that gates whether this dependency applies, evaluated\n * against the depending plugin's own resolved configuration (defaults merged\n * with user config). Return `false` to skip the dependency entirely.\n *\n * Lets a plugin require/recommend another plugin only under certain config\n * — e.g. a plugin that needs a tool-panel host only when its tool panel is\n * enabled. When omitted, the dependency always applies.\n * @example when: (cfg: PivotConfig) => cfg.showToolPanel === true\n * @since 2.16.0\n */\n when?: (pluginConfig: unknown) => 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 \"Both transform the entire row model in different ways\"\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 * @since 1.8.0\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 * @since 1.8.0\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 | 'processConfig'\n | 'processRows'\n | 'afterStructuralRender'\n | 'afterRender'\n | 'afterCellRender'\n | 'afterRowRender'\n | 'onCellClick'\n | 'onCellMouseDown'\n | 'onCellMouseMove'\n | 'onCellMouseUp'\n | 'onHeaderClick'\n | 'onKeyDown'\n | 'onRowClick'\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 * @since 1.1.0\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: 'tree', reason: 'Both transform the entire row model in different ways' },\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 * Whether this plugin's `processRows` hook injects or removes rows\n * (group headers, tree nodes, pivot aggregates, placeholders, etc.).\n *\n * When true, the core sorting module delegates to the render scheduler\n * instead of sorting `_rows` in-place, because `_rows` may contain\n * plugin-generated marker objects that would be corrupted by a direct sort.\n *\n * Plugins that only **filter** or **reorder** existing rows (e.g. Filtering,\n * MultiSort) should leave this unset or set it to `false`.\n */\n modifiesRowStructure?: boolean;\n}\n\n/** Convert camelCase icon key to kebab-case data-icon attribute value. */\nexport function toIconAttr(key: string): string {\n return key.replace(/([A-Z])/g, '-$1').toLowerCase();\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 * @since 0.1.1\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. Optional dependencies are\n * silent by default; set a {@link PluginDependency.severity} of `'warn'` or\n * `'info'` to opt into a development-only log.\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 * Alternative names for backward compatibility.\n * `getPluginByName()` matches against both `name` and `aliases`.\n * @internal\n */\n readonly aliases?: readonly 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 via `document.adoptedStyleSheets` */\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 * Effective configuration (defaults merged with user config), resolved\n * eagerly so it is available **before** {@link attach} runs. Dependency\n * validation happens before `attach()` merges {@link config}, so config\n * -conditional dependency `when` predicates evaluate against this getter.\n *\n * While attached, returns the already-merged {@link config}; otherwise\n * recomputes `defaultConfig` + `userConfig` fresh. `config` is intentionally\n * **not** trusted when detached because {@link detach} does not clear it — a\n * re-validated plugin would otherwise see stale config from a prior\n * attachment. The {@link attach}-managed abort controller is the\n * attached/detached signal.\n * @internal Plugin infrastructure (used by `validatePluginDependencies`).\n */\n get resolvedConfig(): TConfig {\n return (this.#abortController ? this.config : { ...this.defaultConfig, ...this.userConfig }) as TConfig;\n }\n\n /**\n * Merge user-supplied configuration from other plugin instances into this\n * plugin's `userConfig`. Used by `PluginManager.attachAll()`'s alias-collapse\n * pre-pass: when a consumer instantiates the same plugin more than once (or\n * under an alias name), only one instance is attached, and the other\n * instances' configs are folded in via this method.\n *\n * Merge rules (shallow):\n *\n * - Same key, **same value** → silent.\n * - Same key, **different values** (incl. function vs function) → throws\n * a diagnostic naming the conflicting key. Conflicting callbacks\n * (`canDrag`, `canDrop`, etc.) cannot be reconciled automatically.\n * - **Disjoint keys** → merged cleanly.\n *\n * Subclasses may override to implement custom deep-merge logic, but should\n * call `super.mergeConfigsFrom(others)` to inherit the conflict-detection.\n *\n * @internal Plugin infrastructure (used by `PluginManager.attachAll`).\n * @throws Error with diagnostic code `TBW025` when configs conflict.\n */\n mergeConfigsFrom(others: readonly BaseGridPlugin<TConfig>[]): void {\n if (others.length === 0) return;\n const merged = { ...this.userConfig } as Record<string, unknown>;\n const sources: Record<string, BaseGridPlugin<TConfig>> = {};\n for (const key of Object.keys(merged)) sources[key] = this;\n for (const other of others) {\n const otherConfig = other.userConfig as Record<string, unknown>;\n for (const [key, value] of Object.entries(otherConfig)) {\n if (value === undefined) continue;\n if (!(key in merged)) {\n merged[key] = value;\n sources[key] = other;\n continue;\n }\n if (merged[key] === value) continue;\n // Conflict\n const ownerA = sources[key]?.constructor.name ?? this.constructor.name;\n const ownerB = other.constructor.name;\n const code = formatDiagnostic(\n PLUGIN_ALIAS_CONFIG_CONFLICT,\n `Cannot merge plugin configs for \"${this.name}\": conflicting value for \"${key}\" supplied by both ${ownerA} and ${ownerB}. Pass the option on a single instance, or remove the duplicate.`,\n undefined,\n this.name,\n );\n throw new Error(code);\n }\n }\n // Mutate userConfig in place so re-attach picks it up via attach().\n // `userConfig` is declared `readonly` to prevent reassigning the binding,\n // but the object's own properties are intentionally mutable for this\n // alias-collapse pre-pass. A single cast to a record is enough — no\n // `as unknown as` double-cast needed.\n Object.assign(this.userConfig as Record<string, unknown>, merged);\n }\n\n /**\n * Replace this plugin's `userConfig` with a freshly-resolved sibling\n * instance's config. Used by the grid's per-grid feature-instance gate\n * (`#initializePlugins`): when a feature is re-resolved across a config\n * change, the **existing** instance is reused — so its accumulated state\n * survives — but it must adopt the latest `gridConfig.features.<name>`\n * value. Unlike {@link mergeConfigsFrom}, this is last-write-wins with no\n * conflict detection: the re-resolved instance is authoritative.\n *\n * `attach()` recomputes {@link config} from `userConfig`, so updating\n * `userConfig` here is enough for the reused instance to pick up the new\n * config on its next attach during the same `#initializePlugins` pass.\n *\n * @internal Plugin infrastructure (used by the grid feature gate).\n */\n refreshUserConfigFrom(other: BaseGridPlugin<TConfig>): void {\n const target = this.userConfig as Record<string, unknown>;\n // Snapshot the source BEFORE clearing `target`. A feature factory may\n // store the consumer's config object by reference (e.g. `new\n // ContextMenuPlugin(config)`), and a consumer that passes the *same* config\n // object across re-resolutions (common when `gridConfig` is a recomputed\n // Angular `computed()`) makes `this.userConfig === other.userConfig`.\n // Without this snapshot, the `delete` loop below would empty that shared\n // object first, then `Object.assign` would copy back nothing — silently\n // wiping the config and falling back to plugin defaults. (#contextMenu)\n const source = { ...(other.userConfig as Record<string, unknown>) };\n for (const key of Object.keys(target)) delete target[key];\n Object.assign(target, source);\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 * **Prefer {@link BaseGridPlugin.grid grid.getPluginByName()}** when you don't need the class import.\n *\n * @example\n * ```ts\n * // Preferred: by name\n * const selection = this.grid?.getPluginByName('selection');\n *\n * // Alternative: by class\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 * Emit an event to **both** the plugin Event Bus (for inter-plugin communication)\n * **and** the DOM (for external consumers via `addEventListener`).\n *\n * Use this when a state change is relevant to both other plugins and external\n * consumers. For example, `sort-change` needs to invalidate Selection (plugin bus)\n * and notify the host application (DOM event).\n *\n * @category Plugin Development\n * @param eventType - The event type to broadcast\n * @param detail - The event payload\n *\n * @example\n * ```typescript\n * // Notify both plugins and consumers of a sort change\n * this.broadcast('sort-change', { sortModel: [...this.sortModel] });\n * ```\n */\n protected broadcast<T>(eventType: string, detail: T): void {\n this.emitPluginEvent(eventType, detail);\n this.emit(eventType, detail);\n }\n\n /**\n * Run a yes/no plugin query and reduce the responses to a single boolean.\n *\n * `grid.query()` always returns an **array** of responses (empty when no\n * plugin handles the query), and `[]` is truthy in JavaScript — so testing\n * the raw result directly (`if (grid.query(...))`) is always `true` and\n * silently takes the \"some plugin said yes\" branch even when nothing\n * answered. Always route boolean queries through this helper.\n *\n * @category Plugin Development\n * @param type - The query type to dispatch\n * @param context - Optional query context passed to the handlers\n * @returns `true` only when at least one plugin answered truthily\n *\n * @example\n * ```typescript\n * if (this.queryBoolean('datasource:is-active')) {\n * // A ServerSide data source really is installed\n * }\n * ```\n * @since 3.4.0\n */\n protected queryBoolean(type: string, context: unknown = null): boolean {\n const responses = this.grid?.query?.(type, context);\n // Tolerate hosts/tests that hand back a bare value instead of the array.\n return Array.isArray(responses) ? responses.some(Boolean) : !!responses;\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 * Re-render visible rows without rebuilding the row model or recalculating geometry.\n * Use this when row data has been updated in-place (e.g., server-side block loads)\n * and only the visible viewport needs to re-render.\n */\n protected requestVirtualRefresh(): void {\n this.grid?.requestVirtualRefresh?.();\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?._hostElement;\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(): Required<GridIcons> {\n const userIcons = this.grid?.gridConfig?.icons ?? {};\n return { ...DEFAULT_GRID_ICONS, ...userIcons };\n }\n\n /**\n * Resolve a user-visible string from {@link GridConfig.locale}, falling back to\n * the English default supplied at the call site.\n *\n * The fallback is passed inline rather than read from a shipped default map so\n * that a plugin's strings live in the plugin's own chunk.\n *\n * @param key - Namespaced message id, e.g. `'filter.apply'`.\n * @param fallback - English text used when the key is not overridden.\n * @since 3.5.0\n *\n * @example\n * ```ts\n * applyBtn.textContent = this.t('filter.apply', 'Apply');\n * ```\n */\n protected t(key: string, fallback: string): string {\n return this.grid?.effectiveConfig?.locale?.[key] ?? fallback;\n }\n\n /**\n * A bound `t()` suitable for handing to standalone renderer\n * helpers and public renderer callbacks.\n * @since 3.5.0\n */\n protected get translate(): Translate {\n return (key, fallback) => this.t(key, fallback);\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 * Set an icon on an element using the CSS-first hybrid approach.\n *\n * Sets `data-icon` for CSS pseudo-element rendering and `data-expanded` for\n * expand/collapse toggle elements. Only injects DOM content when a JS override\n * is present (via `gridConfig.icons` or `pluginOverride`), which naturally\n * suppresses the CSS `::before` via the `:empty` selector.\n *\n * @param element - The element to set the icon on\n * @param iconKey - The icon key in GridIcons (e.g., 'expand', 'collapse')\n * @param pluginOverride - Optional plugin-level override (string or HTMLElement)\n */\n protected setIcon(element: HTMLElement, iconKey: keyof GridIcons, pluginOverride?: IconValue): void {\n // Set data-icon attribute for CSS targeting (always)\n element.dataset.icon = toIconAttr(iconKey);\n\n // Set data-expanded for toggle elements (tree, group, master-detail)\n if (iconKey === 'collapse') {\n element.dataset.expanded = '';\n } else if (iconKey === 'expand') {\n delete element.dataset.expanded;\n }\n\n // Resolve JS-only icon (returns undefined when CSS default should be used)\n const jsIcon = this.#resolveJsIcon(iconKey, pluginOverride);\n\n if (jsIcon !== undefined) {\n // JS override: inject content (suppresses CSS ::before via :empty)\n if (typeof jsIcon === 'string') {\n setSanitizedHTML(element, jsIcon);\n } else if (jsIcon instanceof HTMLElement) {\n element.replaceChildren(jsIcon.cloneNode(true));\n }\n } else {\n // CSS path: ensure element is empty so ::before renders\n element.innerHTML = '';\n }\n }\n\n /**\n * Resolve JS-only icon override. Returns undefined if CSS default should render.\n * Only returns a value when the user explicitly set gridConfig.icons[key] or\n * the plugin passed an override.\n */\n #resolveJsIcon(iconKey: keyof GridIcons, pluginOverride?: IconValue): IconValue | undefined {\n if (pluginOverride !== undefined) return pluginOverride;\n return this.grid?.gridConfig?.icons?.[iconKey];\n }\n\n /**\n * Create or replace a sort indicator on a header cell.\n *\n * Handles the full lifecycle: removes any existing indicator, creates a new\n * `<span part=\"sort-indicator\" class=\"sort-indicator\">` with the appropriate\n * icon, sets `aria-sort` / `data-sort` attributes, and inserts it before the\n * filter button or resize handle.\n *\n * Plugins decide **which** cells and **when** — this method handles **how**.\n *\n * @param cell - The header cell element\n * @param direction - `'asc'` | `'desc'` for sorted, `null` for unsorted idle state\n * @returns The created indicator element (useful for appending badges, etc.)\n */\n protected updateSortIndicator(cell: Element, direction: 'asc' | 'desc' | null): HTMLElement {\n // Remove any existing indicator (core uses part, plugins use class)\n cell.querySelector('[part~=\"sort-indicator\"], .sort-indicator')?.remove();\n\n const indicator = document.createElement('span');\n indicator.setAttribute('part', 'sort-indicator');\n indicator.className = 'sort-indicator';\n\n if (direction) {\n cell.setAttribute('aria-sort', direction === 'asc' ? 'ascending' : 'descending');\n cell.setAttribute('data-sort', direction);\n this.setIcon(indicator, direction === 'asc' ? 'sortAsc' : 'sortDesc');\n } else {\n cell.setAttribute('aria-sort', 'none');\n cell.removeAttribute('data-sort');\n this.setIcon(indicator, 'sortNone');\n }\n\n // Insert before filter button or resize handle to maintain DOM order\n const insertBefore = cell.querySelector('.tbw-filter-btn') ?? cell.querySelector('.resize-handle');\n if (insertBefore) {\n cell.insertBefore(indicator, insertBefore);\n } else {\n cell.appendChild(indicator);\n }\n\n return indicator;\n }\n\n /**\n * Log a warning with an optional diagnostic code.\n *\n * When a diagnostic code is provided, the message is formatted with the code\n * and a link to the troubleshooting docs.\n *\n * @example\n * ```ts\n * this.warn('Something went wrong'); // plain\n * this.warn(MISSING_BREAKPOINT, 'Set a breakpoint'); // with code + docs link\n * ```\n */\n protected warn(message: string): void;\n protected warn(code: DiagnosticCode, message: string): void;\n protected warn(codeOrMessage: DiagnosticCode | string, message?: string): void {\n if (message !== undefined) {\n // Called with (code, message)\n console.warn(formatDiagnostic(codeOrMessage as DiagnosticCode, message, this.gridElement.id, this.name));\n } else {\n // Called with (message) — plain warning, no diagnostic code\n console.warn(`${gridPrefix(this.gridElement.id, this.name)} ${codeOrMessage}`);\n }\n }\n\n /**\n * Throw an error with a diagnostic code and docs link.\n * Use for configuration errors and API misuse that should halt execution.\n */\n protected throwDiagnostic(code: DiagnosticCode, message: string): never {\n throw new Error(formatDiagnostic(code, message, this.gridElement.id, this.name));\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 * Contribute to the grid's effective configuration during each config merge.\n * Called by {@link ConfigManager.merge} after the base config is assembled,\n * before it becomes `effectiveConfig`. Mutate `config` in place to inject\n * plugin-derived settings (e.g. the shell plugin folds its light-DOM /\n * API-registered state into `config.shell`).\n *\n * This is the config-pipeline counterpart to {@link processColumns} /\n * {@link processRows}: it keeps the core config merge free of any plugin\n * knowledge — each plugin owns the shape it contributes.\n *\n * @param config - The mutable base config being assembled into effectiveConfig.\n *\n * @example\n * ```ts\n * processConfig(config: GridConfig): void {\n * config.shell = { ...config.shell, header: { title: this.#title } };\n * }\n * ```\n */\n processConfig?(config: GridConfig): void;\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-grid-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 synchronously immediately after the grid (re)builds its root DOM\n * structure, before the browser has a chance to paint.\n *\n * Unlike {@link afterRender} (which runs in the scheduler's STYLE phase and\n * may be deferred), this hook fires inside the same task as the structural\n * DOM build. It is the correct place for plugins that need to wrap, relocate,\n * or augment the freshly built grid DOM without a flash of unstyled/unwrapped\n * content — e.g. a shell that moves the grid content into a chrome wrapper.\n *\n * The grid emits this hook every time it performs a full structural rebuild\n * (initial connect and structural changes), but NOT on the scroll/data hot\n * path. Implementations MUST be idempotent: a full rebuild discards any DOM a\n * previous invocation produced, so re-create it, and no-op when the desired\n * structure is already present.\n *\n * @example\n * ```ts\n * afterStructuralRender(): void {\n * const root = this.grid?._renderRoot;\n * if (!root || root.querySelector('.my-wrapper > .tbw-grid-content')) return;\n * // build wrapper, move .tbw-grid-content inside it\n * }\n * ```\n */\n afterStructuralRender?(): void;\n\n /**\n * Called synchronously by the grid immediately before it tears down and\n * rebuilds its root DOM structure (a structural re-render). Plugins that\n * relocated existing light-DOM nodes into their own chrome (e.g. a shell that\n * moves toolbar buttons into the header bar) use this to move those nodes back\n * to their original container so they survive the rebuild. Generic\n * counterpart to {@link afterStructuralRender}. MUST be idempotent.\n */\n beforeStructuralRender?(): void;\n\n /**\n * Called by the grid after a config merge to ask whether the change requires a\n * full structural re-render (rather than a cheaper in-place update). A plugin\n * returns `true` when its rendered DOM signature no longer matches the new\n * effective config — e.g. a shell header appearing/disappearing or a tool\n * panel's count/position/mode changing. The grid performs a structural\n * re-render if ANY plugin returns `true`. MUST be side-effect free.\n */\n needsStructuralRerender?(): boolean;\n\n /**\n * Called by the grid to let a plugin (re-)parse its own light-DOM\n * contributions into plugin state ahead of a config merge. Generic seam that\n * keeps core free of plugin-specific light-DOM tag knowledge (the plugin reads\n * `this.grid._hostElement`). MUST be idempotent.\n */\n parseLightDom?(): void;\n\n /**\n * Called by the grid (from `refreshColumns` and the light-DOM observer) to\n * let a plugin re-parse its light-DOM contributions AND refresh any\n * already-rendered chrome in place when something changed — e.g. a shell\n * title or tool buttons that a framework projected asynchronously after the\n * initial render. Distinct from {@link parseLightDom}, which only updates\n * plugin state. Generic seam — core holds no plugin-specific knowledge. MUST\n * be idempotent and a no-op when the plugin's chrome is not yet rendered.\n */\n syncLightDom?(): 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 * 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 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 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 /**\n * Report vertical scroll boundary offsets for this plugin.\n * Plugins that overlay the top or bottom of the rows viewport (e.g. sticky\n * row clones) should return how much space they obscure on each edge, so\n * keyboard navigation can scroll a focused row clear of the overlay rather\n * than leaving it hidden underneath — WCAG 2.2 SC 2.4.11 Focus Not Obscured.\n *\n * Only report space that is painted *over* the scrollable rows. Content that\n * takes part in normal flow (pinned-row bars, the column header) already\n * shrinks the viewport and must NOT be reported here, or rows would scroll\n * twice as far as needed.\n *\n * @param focusedRowIndex - Index of the row being scrolled into view, so a plugin can report `skipScroll` when it already renders that row in a permanently visible overlay\n * @returns Object with top/bottom pixel offsets and optional skipScroll flag, or undefined if plugin obscures nothing\n *\n * @example\n * ```ts\n * getVerticalScrollOffsets(focusedRowIndex?: number): { top: number; bottom: number; skipScroll?: boolean } | undefined {\n * if (!this.overlayEl) return undefined;\n * // The row is already pinned in the overlay — it is visible where it is.\n * if (focusedRowIndex != null && this.stuckIndices.includes(focusedRowIndex)) {\n * return { top: 0, bottom: 0, skipScroll: true };\n * }\n * return { top: this.overlayEl.offsetHeight, bottom: 0 };\n * }\n * ```\n *\n * @since 3.6.0\n */\n getVerticalScrollOffsets?(\n focusedRowIndex?: number,\n ): { top: number; bottom: 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.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 lockPosition: true,\n utility: true, // Marks this as a utility column (excluded from selection, clipboard, etc.)\n meta: {\n expanderColumn: true,\n expanderPlugin: pluginName,\n },\n };\n}\n","/**\n * Row Grouping Core Logic\n *\n * Pure functions for building grouped row models and aggregations.\n */\n\n/* eslint-disable @typescript-eslint/no-explicit-any */\n\nimport type { DefaultExpandedValue, GroupDefinition, GroupingRowsConfig, GroupRowModelItem, RenderRow } from './types';\n\ninterface GroupNode {\n key: string; // composite key\n value: any;\n depth: number;\n rows: any[];\n children: Map<string, GroupNode>;\n parent?: GroupNode;\n}\n\ninterface BuildGroupingArgs {\n rows: any[];\n config: GroupingRowsConfig;\n expanded: Set<string>;\n /** Initial expanded state to apply (processed by the plugin) */\n initialExpanded?: Set<string>;\n /** Sort direction per group depth level. 1 = ascending, -1 = descending.\n * When omitted, groups at all levels sort ascending. */\n groupSortDirections?: Map<number, 1 | -1>;\n}\n\n/**\n * Build a flattened grouping projection (collapsed by default).\n * Returns empty array when groupOn not configured or all rows ungrouped.\n *\n * @param args - The grouping arguments\n * @returns Flattened array of render rows (groups + data rows)\n */\nexport function buildGroupedRowModel({\n rows,\n config,\n expanded,\n initialExpanded,\n groupSortDirections,\n}: BuildGroupingArgs): RenderRow[] {\n const groupOn = config.groupOn;\n if (typeof groupOn !== 'function') {\n return [];\n }\n\n const root: GroupNode = { key: '__root__', value: null, depth: -1, rows: [], children: new Map() };\n\n // Build tree structure — push each row into every ancestor along the path\n // so that each group's `rows` array contains ALL data rows in its subtree.\n // This is required for correct counts and aggregations on multi-level groups.\n rows.forEach((r) => {\n let path: any = groupOn(r);\n if (path == null || path === false) path = ['__ungrouped__'];\n else if (!Array.isArray(path)) path = [path];\n\n let parent = root;\n path.forEach((rawVal: any, depthIdx: number) => {\n const seg = rawVal == null ? '∅' : String(rawVal);\n const composite = parent.key === '__root__' ? seg : parent.key + '||' + seg;\n let node = parent.children.get(seg);\n if (!node) {\n node = { key: composite, value: rawVal, depth: depthIdx, rows: [], children: new Map(), parent };\n parent.children.set(seg, node);\n }\n node.rows.push(r);\n parent = node;\n });\n });\n\n // All ungrouped? treat as no grouping\n if (root.children.size === 1 && root.children.has('__ungrouped__')) {\n const only = root.children.get('__ungrouped__')!;\n if (only.rows.length === rows.length) return [];\n }\n\n // Pre-build row→index map for O(1) lookups (avoids O(n²) from rows.indexOf)\n const rowIndexMap = new Map<any, number>();\n for (let i = 0; i < rows.length; i++) {\n rowIndexMap.set(rows[i], i);\n }\n\n // Merge expanded sets - use initialExpanded on first render, then expanded takes over\n const effectiveExpanded = new Set([...expanded, ...(initialExpanded ?? [])]);\n\n // Sort sibling groups by their group value so that group header order is\n // deterministic and respects the active sort direction for each depth level.\n // Default: ascending. When a user sorts a grouped column, the corresponding\n // depth level's direction flips, keeping groups in predictable order.\n const sortedChildren = (node: GroupNode): GroupNode[] => {\n const children = [...node.children.values()];\n // Determine direction for this depth level (children are one level deeper than the node)\n const childDepth = node === root ? 0 : node.depth + 1;\n const dir = groupSortDirections?.get(childDepth) ?? 1;\n children.sort((a, b) => {\n const av = a.value;\n const bv = b.value;\n if (av == null && bv == null) return 0;\n if (av == null) return dir;\n if (bv == null) return -dir;\n return av > bv ? dir : av < bv ? -dir : 0;\n });\n return children;\n };\n\n // Flatten tree to array\n const flat: RenderRow[] = [];\n const visit = (node: GroupNode) => {\n if (node === root) {\n for (const c of sortedChildren(node)) visit(c);\n return;\n }\n\n const isExpanded = effectiveExpanded.has(node.key);\n flat.push({\n kind: 'group',\n key: node.key,\n value: node.value,\n depth: node.depth,\n rows: node.rows,\n expanded: isExpanded,\n });\n\n if (isExpanded) {\n if (node.children.size) {\n for (const c of sortedChildren(node)) visit(c);\n } else {\n node.rows.forEach((r) => flat.push({ kind: 'data', row: r, rowIndex: rowIndexMap.get(r) ?? -1 }));\n }\n }\n };\n visit(root);\n\n return flat;\n}\n\n/**\n * Discover which column field produces the group value at each depth level.\n *\n * Samples the first row's `groupOn` output to get the group path, then checks\n * which column fields produce matching values for that row. This mapping allows\n * the plugin to apply user-invoked column sort directions to the correct group\n * depth levels.\n *\n * @returns Map from depth index to column field name, or empty map if unmappable\n */\nexport function resolveGroupFields(\n rows: any[],\n groupOn: (row: any) => any[] | any | null | false,\n columnFields: string[],\n): Map<number, string> {\n const depthToField = new Map<number, string>();\n if (rows.length === 0) return depthToField;\n\n const sampleRow = rows[0];\n let path: any = groupOn(sampleRow);\n if (path == null || path === false) return depthToField;\n if (!Array.isArray(path)) path = [path];\n\n for (let depth = 0; depth < path.length; depth++) {\n const groupValue = path[depth];\n for (const field of columnFields) {\n if (sampleRow[field] === groupValue) {\n depthToField.set(depth, field);\n break;\n }\n }\n }\n return depthToField;\n}\n\n/**\n * Toggle expansion state for a group key.\n *\n * @param expandedKeys - Current set of expanded keys\n * @param key - The group key to toggle\n * @returns New set with toggled state\n */\nexport function toggleGroupExpansion(expandedKeys: Set<string>, key: string): Set<string> {\n const newSet = new Set(expandedKeys);\n if (newSet.has(key)) {\n newSet.delete(key);\n } else {\n newSet.add(key);\n }\n return newSet;\n}\n\n/**\n * Expand all groups.\n *\n * @param rows - The flattened render rows\n * @returns Set of all group keys\n */\nexport function expandAllGroups(rows: RenderRow[]): Set<string> {\n const keys = new Set<string>();\n for (const row of rows) {\n if (row.kind === 'group') {\n keys.add(row.key);\n }\n }\n return keys;\n}\n\n/**\n * Collapse all groups.\n *\n * @returns Empty set\n */\nexport function collapseAllGroups(): Set<string> {\n return new Set();\n}\n\n/**\n * Resolve a defaultExpanded value to a set of keys to expand.\n * This needs to be called AFTER building the group model to get all keys.\n *\n * Note: `plugins/pivot/pivot-engine.ts` has a same-named function with the\n * opposite `undefined` default (expand-all). See the comment there.\n *\n * @param value - The defaultExpanded config value\n * @param allGroupKeys - All group keys from the model\n * @returns Set of keys to expand initially\n */\nexport function resolveDefaultExpanded(value: DefaultExpandedValue, allGroupKeys: string[]): Set<string> {\n if (value === true) {\n // Expand all groups\n return new Set(allGroupKeys);\n }\n if (value === false || value == null) {\n // Collapse all groups\n return new Set();\n }\n if (typeof value === 'number') {\n // Expand group at this index\n const key = allGroupKeys[value];\n return key ? new Set([key]) : new Set();\n }\n if (typeof value === 'string') {\n // Expand group with this key\n return new Set([value]);\n }\n if (Array.isArray(value)) {\n // Expand groups with these keys\n return new Set(value);\n }\n return new Set();\n}\n\n/**\n * Get all group keys from a flattened model.\n *\n * @param rows - The flattened render rows\n * @returns Array of group keys\n */\nexport function getGroupKeys(rows: RenderRow[]): string[] {\n return rows.filter((r): r is GroupRowModelItem => r.kind === 'group').map((r) => r.key);\n}\n\n/**\n * Count total rows in a group (including nested groups).\n *\n * @param groupRow - The group row\n * @returns Total row count\n */\nexport function getGroupRowCount(groupRow: RenderRow): number {\n if (groupRow.kind !== 'group') return 0;\n return groupRow.rows.length;\n}\n\n// #region Pre-Defined Group Model\n\ninterface PreDefinedGroupModelArgs {\n groups: GroupDefinition[];\n expanded: Set<string>;\n groupRows: Map<string, unknown[]>;\n loadingGroups: Set<string>;\n parentPath?: string[];\n}\n\n/**\n * Build a flattened render model from pre-defined group definitions.\n *\n * Unlike `buildGroupedRowModel`, this does not analyze row data — groups\n * are provided externally (e.g. from a server). Row data for each group\n * is populated lazily via the `groupRows` map.\n *\n * @param args - Pre-defined grouping arguments\n * @returns Flattened array of render rows (groups + data rows)\n */\nexport function buildPreDefinedGroupModel({\n groups,\n expanded,\n groupRows,\n loadingGroups,\n parentPath = [],\n}: PreDefinedGroupModelArgs): RenderRow[] {\n const flat: RenderRow[] = [];\n const depth = parentPath.length;\n\n for (const group of groups) {\n const currentPath = [...parentPath, group.key];\n const isExpanded = expanded.has(group.key);\n const rows = groupRows.get(group.key) ?? [];\n const isLoading = loadingGroups.has(group.key);\n\n flat.push({\n kind: 'group',\n key: group.key,\n value: group.value,\n depth,\n rows,\n expanded: isExpanded,\n });\n\n if (isExpanded) {\n // Nested child groups take priority over leaf rows\n if (group.children?.length) {\n const childRows = buildPreDefinedGroupModel({\n groups: group.children,\n expanded,\n groupRows,\n loadingGroups,\n parentPath: currentPath,\n });\n flat.push(...childRows);\n } else if (isLoading) {\n // Loading placeholder — rendered by the plugin as a loading indicator\n flat.push({ kind: 'data', row: { __loading: true, __groupKey: group.key }, rowIndex: -1 });\n } else {\n // Leaf rows from the groupRows map\n rows.forEach((row, idx) => {\n flat.push({ kind: 'data', row, rowIndex: idx });\n });\n }\n }\n }\n\n return flat;\n}\n\n/**\n * Compute the group path (array of ancestor keys) for a given group key\n * within a pre-defined group structure.\n *\n * @param groups - The group definitions to search\n * @param targetKey - The key to find\n * @param parentPath - Accumulated path (used for recursion)\n * @returns Array of group keys from root to target, or empty array if not found\n */\nexport function getGroupPath(groups: GroupDefinition[], targetKey: string, parentPath: string[] = []): string[] {\n for (const group of groups) {\n const currentPath = [...parentPath, group.key];\n if (group.key === targetKey) {\n return currentPath;\n }\n if (group.children?.length) {\n const found = getGroupPath(group.children, targetKey, currentPath);\n if (found.length > 0) return found;\n }\n }\n return [];\n}\n// #endregion\n","@layer tbw-plugins {\n /* Group row layout - uses grid template from parent */\n .group-row {\n display: grid;\n grid-template-columns: var(--tbw-column-template);\n background: var(--tbw-grouping-rows-bg, var(--tbw-color-panel-bg));\n font-weight: 500;\n border-bottom: var(--tbw-row-divider);\n min-height: var(--tbw-row-height);\n }\n .group-row .cell {\n display: flex;\n align-items: center;\n padding: var(--tbw-cell-padding, 0.125rem 0.5rem);\n }\n @media (hover: hover) {\n .group-row:hover {\n background: var(--tbw-grouping-rows-bg-hover, var(--tbw-color-row-hover));\n }\n }\n .group-toggle {\n cursor: pointer;\n user-select: none;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: var(--tbw-toggle-size, 1.25rem);\n height: var(--tbw-toggle-size, 1.25rem);\n margin-right: 0.25rem;\n background: none;\n border: 0;\n font: inherit;\n }\n .group-toggle:hover {\n background: var(--tbw-grouping-rows-toggle-hover, var(--tbw-color-row-hover));\n border-radius: var(--tbw-border-radius, 0.125rem);\n }\n\n /* SC 2.5.8: grow the target, not the box, so the group row keeps its height. */\n .group-toggle {\n position: relative;\n\n &::after {\n content: '';\n position: absolute;\n inset-block-start: 50%;\n inset-inline-start: 50%;\n translate: -50% -50%;\n inline-size: var(--tbw-touch-target-min, 24px);\n block-size: var(--tbw-touch-target-min, 24px);\n }\n }\n\n @media (pointer: coarse) {\n .group-toggle {\n min-width: var(--tbw-touch-target-min, 24px);\n min-height: var(--tbw-touch-target-min, 24px);\n }\n }\n .group-label {\n display: inline-flex;\n align-items: center;\n gap: var(--tbw-panel-gap, var(--tbw-spacing-md, 0.5rem));\n }\n .group-count {\n color: var(--tbw-grouping-rows-count-color, var(--tbw-color-fg-muted));\n font-size: var(--tbw-font-size-xs, 0.85em);\n font-weight: normal;\n }\n\n /* Aggregates container (for full-width mode) */\n .group-aggregates {\n display: inline-flex;\n align-items: center;\n gap: var(--tbw-spacing-lg, 1rem);\n margin-left: var(--tbw-spacing-lg, 1rem);\n font-weight: normal;\n font-size: var(--tbw-font-size-sm, 0.875em);\n color: var(--tbw-grouping-rows-aggregate-color, var(--tbw-color-fg-muted));\n }\n\n .group-aggregate {\n white-space: nowrap;\n }\n\n /* Depth-based indentation using CSS custom property */\n .group-row {\n padding-left: calc(var(--tbw-group-depth, 0) * var(--tbw-group-indent-width, 1.25em));\n }\n\n /* Slide animation for child rows appearing */\n .data-grid-row.tbw-group-slide-in {\n animation: tbw-group-slide-in var(--tbw-animation-duration, 200ms) var(--tbw-animation-easing, ease-out) forwards;\n }\n\n @keyframes tbw-group-slide-in {\n from {\n opacity: 0;\n transform: translateX(-8px);\n }\n to {\n opacity: 1;\n transform: translateX(0);\n }\n }\n\n /* Fade animation for child rows appearing */\n .data-grid-row.tbw-group-fade-in {\n animation: tbw-group-fade-in var(--tbw-animation-duration, 200ms) var(--tbw-animation-easing, ease-out) forwards;\n }\n\n @keyframes tbw-group-fade-in {\n from {\n opacity: 0;\n }\n to {\n opacity: 1;\n }\n }\n} /* End @layer tbw-plugins */\n","/**\n * Row Grouping Plugin (Class-based)\n *\n * Enables hierarchical row grouping with expand/collapse and aggregations.\n */\n\nimport { GridClasses } from '../../core/constants';\nimport { aggregatorRegistry } from '../../core/internal/aggregators';\nimport { announce, getA11yMessage } from '../../core/internal/aria';\nimport { setRowLoadingState } from '../../core/internal/loading';\nimport { setSanitizedHTML } from '../../core/internal/sanitize';\nimport {\n BaseGridPlugin,\n CellClickEvent,\n HeaderClickEvent,\n type GridElement,\n type PluginManifest,\n type PluginQuery,\n} from '../../core/plugin/base-plugin';\nimport { isExpanderColumn } from '../../core/plugin/expander-column';\nimport type { RowElementInternal } from '../../core/types';\nimport type {\n DataSourceChildrenDetail,\n DataSourceDataDetail,\n FetchChildrenQuery,\n ViewportMappingQuery,\n ViewportMappingResponse,\n} from '../server-side/datasource-types';\nimport {\n buildGroupedRowModel,\n buildPreDefinedGroupModel,\n collapseAllGroups,\n expandAllGroups,\n getGroupKeys,\n getGroupPath,\n getGroupRowCount,\n resolveDefaultExpanded,\n resolveGroupFields,\n toggleGroupExpansion,\n} from './grouping-rows';\nimport styles from './grouping-rows.css?inline';\nimport type {\n DefaultExpandedValue,\n ExpandCollapseAnimation,\n GroupCollapseDetail,\n GroupDefinition,\n GroupExpandDetail,\n GroupingRowsConfig,\n GroupRowModelItem,\n GroupToggleDetail,\n RenderRow,\n} from './types';\n\n/**\n * Group state information returned by getGroupState()\n * @since 0.1.1\n */\nexport interface GroupState {\n /** Whether grouping is currently active */\n isActive: boolean;\n /** Number of expanded groups */\n expandedCount: number;\n /** Total number of groups */\n totalGroups: number;\n /** Array of expanded group keys */\n expandedKeys: string[];\n}\n\n/**\n * Row Grouping Plugin for tbw-grid\n *\n * Organizes rows into collapsible hierarchical groups. Perfect for organizing data\n * by category, department, status, or any other dimension—or even multiple dimensions\n * for nested grouping. Includes aggregation support for summarizing group data.\n *\n * ## Installation\n *\n * ```ts\n * import { GroupingRowsPlugin } from '@toolbox-web/grid/plugins/grouping-rows';\n * ```\n *\n * ## CSS Custom Properties\n *\n * | Property | Default | Description |\n * |----------|---------|-------------|\n * | `--tbw-group-indent-width` | `1.25em` | Indentation per group level |\n * | `--tbw-grouping-rows-bg` | `var(--tbw-color-panel-bg)` | Group row background |\n * | `--tbw-grouping-rows-count-color` | `var(--tbw-color-fg-muted)` | Count badge color |\n * | `--tbw-animation-duration` | `200ms` | Expand/collapse animation |\n *\n * @example Single-Level Grouping by Department\n * ```ts\n * import { queryGrid } from '@toolbox-web/grid';\n * import { GroupingRowsPlugin } from '@toolbox-web/grid/plugins/grouping-rows';\n *\n * const grid = queryGrid('tbw-grid');\n * grid.gridConfig = {\n * columns: [\n * { field: 'name', header: 'Employee' },\n * { field: 'department', header: 'Department' },\n * { field: 'salary', header: 'Salary', type: 'currency' },\n * ],\n * plugins: [\n * new GroupingRowsPlugin({\n * groupOn: (row) => [row.department],\n * showRowCount: true,\n * defaultExpanded: false,\n * }),\n * ],\n * };\n * ```\n *\n * @example Multi-Level Grouping\n * ```ts\n * new GroupingRowsPlugin({\n * groupOn: (row) => [row.region, row.department, row.team],\n * indentWidth: 24,\n * animation: 'slide',\n * })\n * ```\n *\n * @see {@link GroupingRowsConfig} for all configuration options\n * @see {@link GroupState} for the group state structure\n *\n * @internal Extends BaseGridPlugin\n * @since 0.1.1\n */\nexport class GroupingRowsPlugin extends BaseGridPlugin<GroupingRowsConfig> {\n /**\n * Plugin manifest - declares configuration validation rules and events.\n * @internal\n */\n static override readonly manifest: PluginManifest<GroupingRowsConfig> = {\n modifiesRowStructure: true,\n hookPriority: {\n processRows: 10, // Run after ServerSide (-10) so we receive managedNodes[]\n // Run before MultiSort so we can intercept header clicks on grouped columns\n onHeaderClick: -1,\n },\n incompatibleWith: [\n {\n name: 'tree',\n reason:\n 'Both plugins transform the entire row model. TreePlugin flattens nested hierarchies while ' +\n 'GroupingRowsPlugin groups flat rows with synthetic headers. Use one approach per grid.',\n },\n {\n name: 'pivot',\n reason:\n 'PivotPlugin creates its own aggregated row and column structure. ' +\n 'Row grouping cannot be applied on top of pivot-generated rows.',\n },\n ],\n events: [\n {\n type: 'group-toggle',\n description: 'Emitted when groups are expanded/collapsed. Broadcast to both DOM consumers and plugin bus.',\n },\n {\n type: 'group-expand',\n description: 'Emitted when a pre-defined group is expanded.',\n },\n {\n type: 'group-collapse',\n description: 'Emitted when a pre-defined group is collapsed.',\n },\n ],\n queries: [\n {\n type: 'canMoveRow',\n description: 'Returns false for group header rows (cannot be reordered)',\n },\n {\n type: 'grouping:get-grouped-fields',\n description: 'Returns the column field names that match group depth levels (string[])',\n },\n {\n type: 'datasource:viewport-mapping',\n description: 'Translates flat viewport row indices to top-level group indices for ServerSide pagination.',\n },\n ],\n configRules: [\n {\n id: 'groupingRows/accordion-defaultExpanded',\n severity: 'warn',\n message:\n `\"accordion: true\" and \"defaultExpanded\" (non-false) are used together.\\n` +\n ` → In accordion mode, only one group can be open at a time.\\n` +\n ` → Using defaultExpanded with multiple groups will collapse to one on first toggle.\\n` +\n ` → Consider using \"defaultExpanded: false\" or a single group key/index with accordion mode.`,\n check: (config) =>\n config.accordion === true &&\n config.defaultExpanded !== false &&\n config.defaultExpanded !== undefined &&\n // Allow single group expansion with accordion\n !(typeof config.defaultExpanded === 'number') &&\n !(typeof config.defaultExpanded === 'string') &&\n // Warn if true or array with multiple items\n (config.defaultExpanded === true ||\n (Array.isArray(config.defaultExpanded) && config.defaultExpanded.length > 1)),\n },\n ],\n };\n\n /**\n * Optional dependency on MultiSort for coordinated sort management.\n * When MultiSort is loaded, GroupingRows queries its sort model to determine\n * group header ordering. Without it, falls back to core sort state.\n */\n static override readonly dependencies = [\n { name: 'multiSort', required: false, reason: 'Queries sort model for coordinated group sorting' },\n { name: 'serverSide', required: false, reason: 'Consumes datasource events for lazy-loaded grouped data' },\n ];\n\n /** @internal */\n readonly name = 'groupingRows';\n /** @internal */\n override readonly styles = styles;\n\n /** @internal */\n protected override get defaultConfig(): Partial<GroupingRowsConfig> {\n return {\n defaultExpanded: false,\n showRowCount: true,\n indentWidth: 20,\n aggregators: {},\n animation: 'slide',\n accordion: false,\n };\n }\n\n // #region Internal State\n private expandedKeys: Set<string> = new Set();\n private flattenedRows: RenderRow[] = [];\n private isActive = false;\n private previousVisibleKeys = new Set<string>();\n private keysToAnimate = new Set<string>();\n /** Track if initial defaultExpanded has been applied */\n private hasAppliedDefaultExpanded = false;\n /**\n * Expansion override to apply on the next `processRows` rebuild instead of\n * — or in addition to — `config.defaultExpanded`. Set by `setGroupOn(fn, expanded)`\n * and by `expandAll()` / `collapseAll()` when the grouping config has just\n * changed (so `flattenedRows` is stale). Cleared once consumed by a rebuild.\n * See issue #335.\n */\n private pendingExpansion: DefaultExpandedValue | undefined;\n /**\n * Set by `setGroupOn` to mark the cached `flattenedRows` as stale until the\n * next `processRows` runs. While true, `expandAll`/`collapseAll` defer via\n * `pendingExpansion` instead of reading the stale snapshot. See issue #335.\n */\n private groupConfigDirty = false;\n /** Pre-defined group definitions (from config, setGroups(), or datasource:data) */\n private preDefinedGroups: GroupDefinition[] = [];\n /** Row data keyed by group key (from setGroupRows() or datasource:children) */\n private groupRowsMap = new Map<string, unknown[]>();\n /** Groups currently in a loading state */\n private loadingGroups = new Set<string>();\n /** Column fields that produce group values (depth 0, 1, ...). Cached for\n * the `grouping:get-grouped-fields` query so MultiSort can filter them out. */\n private groupedFields: string[] = [];\n /** User-specified sort directions per group depth level. Toggled via\n * header clicks on grouped columns. */\n private userGroupSortDirections = new Map<number, 1 | -1>();\n /**\n * Per-flatten-index ARIA position metadata for `aria-level` /\n * `aria-setsize` / `aria-posinset` (WAI-ARIA Treegrid pattern). Computed\n * once per `flattenedRows` rebuild by {@link computeFlatMeta}; consumed\n * by {@link afterRender} on every visible row.\n */\n private flatMeta: Array<{ level: number; setSize: number; posInSet: number }> = [];\n // #endregion\n\n // #region Animation\n\n /**\n * Get expand/collapse animation style from plugin config.\n * Uses base class isAnimationEnabled to respect grid-level settings.\n */\n private get animationStyle(): ExpandCollapseAnimation {\n if (!this.isAnimationEnabled) return false;\n return this.config.animation ?? 'slide';\n }\n\n // #endregion\n\n // #region Lifecycle\n\n /** @internal */\n override detach(): void {\n // Restore default `role=\"grid\"` on the rows-body so the grid stays\n // ARIA-valid after the plugin is removed (template default lives in\n // `core/internal/dom-builder.ts`). See WAI-ARIA Treegrid pattern.\n const rowsBody = this.gridElement?.querySelector('.rows-body');\n rowsBody?.setAttribute('role', 'grid');\n\n this.expandedKeys.clear();\n this.flattenedRows = [];\n this.flatMeta = [];\n this.isActive = false;\n this.previousVisibleKeys.clear();\n this.keysToAnimate.clear();\n this.hasAppliedDefaultExpanded = false;\n this.pendingExpansion = undefined;\n this.groupConfigDirty = false;\n this.preDefinedGroups = [];\n this.groupRowsMap.clear();\n this.loadingGroups.clear();\n this.groupedFields = [];\n this.userGroupSortDirections.clear();\n }\n\n /**\n * Provide row height for group header rows.\n *\n * If `groupRowHeight` is configured, returns that value for group rows.\n * This allows the variable row height system to use known heights for\n * group headers without needing to measure them from the DOM.\n *\n * @param row - The row object (may be a group row)\n * @param _index - Index in the processed rows array (unused)\n * @returns Height in pixels for group rows, undefined for data rows\n *\n * @internal Plugin hook for variable row height support\n */\n override getRowHeight(row: unknown, _index: number): number | undefined {\n // Only provide height if groupRowHeight is configured\n if (this.config.groupRowHeight == null) return undefined;\n\n // Check if this is a group row\n if ((row as { __isGroupRow?: boolean }).__isGroupRow === true) {\n return this.config.groupRowHeight;\n }\n\n return undefined;\n }\n\n /**\n * Handle plugin queries.\n * @internal\n */\n override handleQuery(query: PluginQuery): unknown {\n if (query.type === 'canMoveRow') {\n // Group header rows cannot be reordered\n const row = query.context as { __isGroupRow?: boolean } | null | undefined;\n if (row?.__isGroupRow === true) {\n return false;\n }\n }\n if (query.type === 'grouping:get-grouped-fields') {\n return [...this.groupedFields];\n }\n if (query.type === 'datasource:viewport-mapping') {\n // Translate visible flat row indices → top-level group indices for ServerSide pagination\n const { viewportStart, viewportEnd } = query.context as ViewportMappingQuery;\n if (this.flattenedRows.length === 0) return undefined;\n // Only respond in pre-defined groups mode (when datasource data is being consumed)\n if (this.preDefinedGroups.length === 0 && !Array.isArray(this.config.groups)) return undefined;\n\n const activeGroups = this.getActiveGroups();\n const startNode = this.getTopLevelGroupIndex(viewportStart);\n const endNode = this.getTopLevelGroupIndex(viewportEnd) + 1; // exclusive\n const totalLoadedNodes = activeGroups.length;\n\n return { startNode, endNode, totalLoadedNodes } satisfies ViewportMappingResponse;\n }\n return undefined;\n }\n\n /**\n * Translate a flat row index to the top-level group index it belongs to.\n * Walks flattenedRows up to the given index and counts depth-0 groups encountered.\n */\n private getTopLevelGroupIndex(flatIndex: number): number {\n let groupIndex = -1;\n const clampedIndex = Math.min(flatIndex, this.flattenedRows.length - 1);\n for (let i = 0; i <= clampedIndex; i++) {\n const row = this.flattenedRows[i];\n if (row.kind === 'group' && row.depth === 0) {\n groupIndex++;\n }\n }\n return Math.max(0, groupIndex);\n }\n\n // #region Lifecycle\n\n /** @internal */\n override attach(grid: GridElement): void {\n super.attach(grid);\n\n // Listen for datasource:data from ServerSidePlugin — claim data as group definitions\n this.on('datasource:data', (detail: unknown) => {\n const d = detail as DataSourceDataDetail;\n if (!d.claimed) {\n d.claimed = true;\n // Interpret incoming rows as group definitions\n this.preDefinedGroups = d.rows as unknown as GroupDefinition[];\n this.groupRowsMap.clear();\n this.loadingGroups.clear();\n this.hasAppliedDefaultExpanded = false;\n this.requestRender();\n }\n });\n\n // Listen for datasource:children — consume child rows from ServerSide\n this.on('datasource:children', (detail: unknown) => {\n const d = detail as DataSourceChildrenDetail;\n if (d.context?.source !== 'grouping-rows') return;\n d.claimed = true;\n\n // Store the rows for the group and clear loading state\n const groupKey = d.context.groupKey as string;\n if (groupKey) {\n this.groupRowsMap.set(groupKey, d.rows);\n this.loadingGroups.delete(groupKey);\n this.requestRender();\n }\n });\n }\n\n /**\n * Intercept header clicks on grouped columns to toggle group sort direction.\n * Returns `true` for grouped columns to prevent MultiSort from handling them.\n * @internal\n */\n override onHeaderClick(event: HeaderClickEvent): boolean | void {\n const fieldIndex = this.groupedFields.indexOf(event.field);\n if (fieldIndex === -1) return; // Not a grouped column — let other plugins handle it\n\n if (!event.column.sortable) return;\n\n // The fieldIndex in groupedFields corresponds to the group depth level\n // (resolveGroupFields populates them in depth order: 0, 1, 2, ...)\n const targetDepth = fieldIndex;\n\n // Toggle direction: asc → desc → asc\n const current = this.userGroupSortDirections.get(targetDepth) ?? 1;\n this.userGroupSortDirections.set(targetDepth, current === 1 ? -1 : 1);\n\n this.requestRender();\n return true; // Prevent MultiSort from handling this column\n }\n // #endregion\n\n // #region Sort State Resolution\n\n /**\n * Build a sort-direction map for each group depth level by cross-referencing\n * the active sort state with the column fields that produce group values.\n *\n * Supports three direction sources (in priority order):\n * 1. User-set directions via header clicks on grouped columns\n * 2. MultiSort plugin model (for backwards compatibility / state restore)\n * 3. Core single-column sort state\n */\n private resolveGroupSortDirections(rows: readonly any[]): Map<number, 1 | -1> | undefined {\n const config = this.config;\n if (typeof config.groupOn !== 'function' || rows.length === 0) {\n this.groupedFields = [];\n return undefined;\n }\n\n // Discover depth → field mapping by sampling rows\n const columnFields = this.columns.map((c) => c.field);\n const depthToField = resolveGroupFields([...rows], config.groupOn, columnFields);\n\n // Cache grouped field names for the grouping:get-grouped-fields query\n this.groupedFields = [...depthToField.values()];\n\n if (depthToField.size === 0) return undefined;\n\n // Start with user-set directions (from header clicks on grouped columns)\n const directions = new Map<number, 1 | -1>(this.userGroupSortDirections);\n\n // Fill missing depths from MultiSort model or core sort state\n if (directions.size < depthToField.size) {\n const activeSorts = new Map<string, 1 | -1>();\n\n const multiSortResults = this.grid?.query?.('sort:get-model', null);\n if (Array.isArray(multiSortResults) && multiSortResults.length > 0) {\n const sortModel = multiSortResults[0] as Array<{ field: string; direction: 'asc' | 'desc' }>;\n if (Array.isArray(sortModel)) {\n for (const entry of sortModel) {\n activeSorts.set(entry.field, entry.direction === 'desc' ? -1 : 1);\n }\n }\n }\n\n if (activeSorts.size === 0) {\n const sortState = this.grid._sortState;\n if (sortState) {\n activeSorts.set(sortState.field, sortState.direction);\n }\n }\n\n for (const [depth, field] of depthToField) {\n if (!directions.has(depth)) {\n const dir = activeSorts.get(field);\n if (dir !== undefined) {\n directions.set(depth, dir);\n }\n }\n }\n }\n\n return directions.size > 0 ? directions : undefined;\n }\n\n // #endregion\n\n // #region Hooks\n\n /**\n * Auto-detect grouping configuration from grid config.\n * Called by plugin system to determine if plugin should activate.\n */\n static detect(rows: readonly any[], config: any): boolean {\n return (\n typeof config?.groupOn === 'function' ||\n typeof config?.enableRowGrouping === 'boolean' ||\n Array.isArray(config?.groups)\n );\n }\n\n /** @internal */\n override processRows(rows: readonly any[]): any[] {\n // Snapshot + clear deferred-expansion state at the top so any early-return\n // (no groupOn, empty rows, etc.) doesn't leave a stale `pendingExpansion`\n // behind to be (mis-)applied to the next unrelated rebuild.\n const pendingExpansion = this.pendingExpansion;\n this.pendingExpansion = undefined;\n this.groupConfigDirty = false;\n\n // Pre-defined groups path — use external group structure instead of groupOn analysis\n if (this.preDefinedGroups.length > 0 || Array.isArray(this.config.groups)) {\n if (this.preDefinedGroups.length > 0 || (Array.isArray(this.config.groups) && this.config.groups.length > 0)) {\n return this.processPreDefinedGroups();\n }\n this.isActive = false;\n this.flattenedRows = [];\n return [];\n }\n\n const config = this.config;\n\n // Check if grouping is configured\n if (typeof config.groupOn !== 'function') {\n this.isActive = false;\n this.flattenedRows = [];\n return [...rows];\n }\n\n // First build: get structure to know all group keys\n // (needed for index-based defaultExpanded)\n const groupSortDirections = this.resolveGroupSortDirections(rows);\n const initialBuild = buildGroupedRowModel({\n rows: [...rows],\n config: config,\n expanded: new Set(), // Empty to get all root groups\n groupSortDirections,\n });\n\n // If no grouping produced, return original rows\n if (initialBuild.length === 0) {\n this.isActive = false;\n this.flattenedRows = [];\n return [...rows];\n }\n\n // Determine which expansion config to resolve against the *fresh* group\n // keys. A pending value (from `setGroupOn(fn, expanded)` or a deferred\n // `expandAll`/`collapseAll` after `setGroupOn`) takes priority and is\n // applied unconditionally — including the empty case (collapseAll). Falls\n // back to the first-render `defaultExpanded` resolution.\n let expansionConfig: DefaultExpandedValue | undefined;\n const isPendingApply = pendingExpansion !== undefined;\n if (isPendingApply) {\n expansionConfig = pendingExpansion;\n } else if (!this.hasAppliedDefaultExpanded && this.expandedKeys.size === 0 && config.defaultExpanded !== false) {\n expansionConfig = config.defaultExpanded ?? false;\n }\n\n let initialExpanded: Set<string> | undefined;\n if (expansionConfig !== undefined) {\n const allKeys = getGroupKeys(initialBuild);\n initialExpanded = resolveDefaultExpanded(expansionConfig, allKeys);\n if (isPendingApply) {\n // Pending overrides any stale keys — apply unconditionally.\n this.expandedKeys = new Set(initialExpanded);\n this.hasAppliedDefaultExpanded = true;\n } else if (initialExpanded.size > 0) {\n this.expandedKeys = new Set(initialExpanded);\n this.hasAppliedDefaultExpanded = true;\n }\n }\n\n // Build with proper expanded state\n const grouped = buildGroupedRowModel({\n rows: [...rows],\n config: config,\n expanded: this.expandedKeys,\n initialExpanded,\n groupSortDirections,\n });\n\n this.isActive = true;\n this.flattenedRows = grouped;\n this.computeFlatMeta();\n\n // Notify subscribers when a deferred expansion has been applied so React/\n // host code can mirror the new state without a follow-up call. Use\n // `broadcast` so DOM listeners (`grid.addEventListener('group-toggle')`)\n // see the change too — matches the manifest contract for `group-toggle`\n // and `toggle()`'s emit pattern.\n if (isPendingApply) {\n this.broadcast<GroupToggleDetail>('group-toggle', { expandedKeys: [...this.expandedKeys] });\n }\n\n // Track which data rows are newly visible (for animation)\n this.keysToAnimate.clear();\n const currentVisibleKeys = new Set<string>();\n grouped.forEach((item, idx) => {\n if (item.kind === 'data') {\n const key = `data-${idx}`;\n currentVisibleKeys.add(key);\n if (!this.previousVisibleKeys.has(key)) {\n this.keysToAnimate.add(key);\n }\n }\n });\n this.previousVisibleKeys = currentVisibleKeys;\n\n // Return flattened rows for rendering\n // The grid will need to handle group rows specially\n return grouped.map((item) => {\n if (item.kind === 'group') {\n return {\n __isGroupRow: true,\n __groupKey: item.key,\n __groupValue: item.value,\n __groupDepth: item.depth,\n __groupRows: item.rows,\n __groupExpanded: item.expanded,\n __groupRowCount: getGroupRowCount(item),\n // Cache key for variable row height support - survives expand/collapse\n __rowCacheKey: `group:${item.key}`,\n };\n }\n return item.row;\n });\n }\n\n /** @internal */\n override onCellClick(event: CellClickEvent): boolean | void {\n const row = event.row as Record<string, unknown> | undefined;\n\n // Check if this is a group row toggle\n if (row?.__isGroupRow) {\n const target = event.originalEvent.target as HTMLElement;\n if (target?.closest(`.${GridClasses.GROUP_TOGGLE}`)) {\n this.toggle(row.__groupKey as string);\n return true; // Prevent default\n }\n }\n }\n\n /** @internal */\n override onKeyDown(event: KeyboardEvent): boolean | void {\n // SPACE toggles expansion on group rows\n if (event.key !== ' ') return;\n\n const focusRow = this.grid._focusRow;\n const row = this.rows[focusRow] as Record<string, unknown> | undefined;\n\n // Only handle SPACE on group rows\n if (!row?.__isGroupRow) return;\n\n event.preventDefault();\n this.toggle(row.__groupKey as string);\n\n // Restore focus styling after render completes via render pipeline\n this.requestRenderWithFocus();\n return true;\n }\n\n /**\n * Render a row. Returns true if we handled the row (group row or loading placeholder), false otherwise.\n * @internal\n */\n override renderRow(row: any, rowEl: HTMLElement, _rowIndex: number): boolean {\n // Handle loading placeholder rows for pre-defined groups\n // Uses the grid's built-in row loading API for consistent, customizable spinners\n if (row?.__loading === true && row?.__groupKey) {\n rowEl.className = 'data-grid-row';\n (rowEl as RowElementInternal).__isCustomRow = true;\n rowEl.innerHTML = '';\n const cell = document.createElement('div');\n cell.className = 'cell';\n cell.style.gridColumn = '1 / -1';\n cell.setAttribute('role', 'gridcell');\n cell.textContent = '\\u00A0'; // &nbsp; for row height\n rowEl.appendChild(cell);\n setRowLoadingState(rowEl, true);\n return true;\n }\n\n // Only handle group rows\n if (!row?.__isGroupRow) {\n return false;\n }\n\n const config = this.config;\n\n // If a custom renderer is provided, use it\n if (config.groupRowRenderer) {\n const toggleExpand = () => {\n this.toggle(row.__groupKey);\n };\n\n const result = config.groupRowRenderer({\n key: row.__groupKey,\n value: row.__groupValue,\n depth: row.__groupDepth,\n rows: row.__groupRows,\n expanded: row.__groupExpanded,\n toggleExpand,\n });\n\n if (result) {\n rowEl.className = 'data-grid-row group-row';\n (rowEl as RowElementInternal).__isCustomRow = true; // Mark for proper class reset on recycle\n rowEl.setAttribute('data-group-depth', String(row.__groupDepth));\n if (typeof result === 'string') {\n setSanitizedHTML(rowEl, result);\n } else {\n rowEl.replaceChildren(result);\n }\n return true;\n }\n }\n\n // Helper to toggle expansion\n const handleToggle = () => {\n this.toggle(row.__groupKey);\n };\n\n // Default group row rendering - keep data-grid-row class for focus/keyboard navigation\n rowEl.className = 'data-grid-row group-row';\n (rowEl as RowElementInternal).__isCustomRow = true; // Mark for proper class reset on recycle\n rowEl.setAttribute('data-group-depth', String(row.__groupDepth));\n rowEl.setAttribute('role', 'row');\n rowEl.setAttribute('aria-expanded', String(row.__groupExpanded));\n // Public theming hook — see also TreePlugin / MasterDetailPlugin.\n rowEl.classList.toggle('tbw-row-expanded', !!row.__groupExpanded);\n // WAI-ARIA Treegrid: group rows announce their hierarchical position.\n const groupMeta = this.flatMeta[_rowIndex];\n if (groupMeta) {\n rowEl.setAttribute('aria-level', String(groupMeta.level));\n rowEl.setAttribute('aria-setsize', String(groupMeta.setSize));\n rowEl.setAttribute('aria-posinset', String(groupMeta.posInSet));\n }\n // Use CSS variable for depth-based indentation\n rowEl.style.setProperty('--tbw-group-depth', String(row.__groupDepth || 0));\n if (config.indentWidth !== undefined) {\n rowEl.style.setProperty('--tbw-group-indent-width', `${config.indentWidth}px`);\n }\n // Clear any inline height from previous use (e.g., responsive card mode sets height: auto)\n // This ensures group rows use CSS-defined height, not stale inline styles from recycled elements\n rowEl.style.height = '';\n rowEl.innerHTML = '';\n\n const isFullWidth = config.fullWidth !== false; // default true\n\n if (isFullWidth) {\n this.renderFullWidthGroupRow(row, rowEl, handleToggle);\n } else {\n this.renderPerColumnGroupRow(row, rowEl, handleToggle);\n }\n\n return true;\n }\n\n /** @internal */\n override afterRender(): void {\n const body = this.gridElement?.querySelector('.rows');\n if (!body) return;\n\n // Hierarchy is now in play \\u2192 swap the rows-body role from `grid` to\n // `treegrid` per WAI-ARIA so per-row level/setsize/posinset are valid\n // in context. Idempotent on the hot path.\n const rowsBody = this.gridElement?.querySelector('.rows-body');\n if (rowsBody && rowsBody.getAttribute('role') !== 'treegrid') {\n rowsBody.setAttribute('role', 'treegrid');\n }\n\n // Apply ARIA position metadata to every visible DATA row (group rows are\n // populated by `renderRow` directly since they bypass the default cell\n // template that exposes `data-row`).\n for (const rowEl of body.querySelectorAll('.data-grid-row:not(.group-row)')) {\n const cell = rowEl.querySelector('.cell[data-row]');\n const idx = cell ? parseInt(cell.getAttribute('data-row') ?? '-1', 10) : -1;\n const meta = this.flatMeta[idx];\n if (!meta) continue;\n rowEl.setAttribute('aria-level', String(meta.level));\n rowEl.setAttribute('aria-setsize', String(meta.setSize));\n rowEl.setAttribute('aria-posinset', String(meta.posInSet));\n }\n\n const style = this.animationStyle;\n if (style === false || this.keysToAnimate.size === 0) return;\n\n const animClass = style === 'fade' ? 'tbw-group-fade-in' : 'tbw-group-slide-in';\n for (const rowEl of body.querySelectorAll('.data-grid-row:not(.group-row)')) {\n const cell = rowEl.querySelector('.cell[data-row]');\n const idx = cell ? parseInt(cell.getAttribute('data-row') ?? '-1', 10) : -1;\n const item = this.flattenedRows[idx];\n const key = item?.kind === 'data' ? `data-${idx}` : undefined;\n\n if (key && this.keysToAnimate.has(key)) {\n rowEl.classList.add(animClass);\n rowEl.addEventListener('animationend', () => rowEl.classList.remove(animClass), { once: true });\n }\n }\n this.keysToAnimate.clear();\n }\n\n /**\n * Compute per-row ARIA hierarchy metadata (`aria-level` / `aria-setsize` /\n * `aria-posinset`) from `flattenedRows`. Two-pass over the flat list:\n * first pass tallies group siblings per (parent-prefix, depth) and counts\n * the data rows that follow each group header; second pass assigns per-row\n * meta. Called once per `flattenedRows` rebuild (not per render).\n */\n private computeFlatMeta(): void {\n const flat = this.flattenedRows;\n const meta: Array<{ level: number; setSize: number; posInSet: number }> = new Array(flat.length);\n\n // Pass 1: count group siblings per (parent-prefix, depth) and direct\n // data-row children per group (used as `setSize` for data rows).\n const groupSiblingCount = new Map<string, number>();\n const groupDataCount = new Map<string, number>();\n let activeGroup: GroupRowModelItem | null = null;\n let activeDataCount = 0;\n for (const item of flat) {\n if (item.kind === 'group') {\n if (activeGroup) groupDataCount.set(activeGroup.key, activeDataCount);\n activeGroup = item;\n activeDataCount = 0;\n const parts = item.key.split('||');\n const parent = parts.slice(0, -1).join('||');\n const k = `${parent}@${item.depth}`;\n groupSiblingCount.set(k, (groupSiblingCount.get(k) ?? 0) + 1);\n } else {\n activeDataCount++;\n }\n }\n if (activeGroup) groupDataCount.set(activeGroup.key, activeDataCount);\n\n // Pass 2: assign per-row level/setsize/posinset.\n const groupSiblingPos = new Map<string, number>();\n let currentGroup: GroupRowModelItem | null = null;\n let dataPos = 0;\n for (let i = 0; i < flat.length; i++) {\n const item = flat[i];\n if (item.kind === 'group') {\n const parts = item.key.split('||');\n const parent = parts.slice(0, -1).join('||');\n const k = `${parent}@${item.depth}`;\n const pos = (groupSiblingPos.get(k) ?? 0) + 1;\n groupSiblingPos.set(k, pos);\n meta[i] = { level: item.depth + 1, setSize: groupSiblingCount.get(k) ?? 1, posInSet: pos };\n currentGroup = item;\n dataPos = 0;\n } else {\n dataPos++;\n const baseDepth = currentGroup?.depth ?? -1;\n const setSize = currentGroup ? (groupDataCount.get(currentGroup.key) ?? 1) : flat.length;\n meta[i] = { level: baseDepth + 2, setSize, posInSet: dataPos };\n }\n }\n\n this.flatMeta = meta;\n }\n // #endregion\n\n // #region Pre-Defined Groups\n\n /**\n * Build the row model from pre-defined group definitions.\n * Used when `groups` config or `setGroups()` provides external group structure.\n */\n private processPreDefinedGroups(): any[] {\n const groups =\n this.preDefinedGroups.length > 0\n ? this.preDefinedGroups\n : Array.isArray(this.config.groups)\n ? this.config.groups\n : [];\n\n if (groups.length === 0) {\n this.isActive = false;\n this.flattenedRows = [];\n return [];\n }\n\n // Resolve defaultExpanded on first render only\n if (!this.hasAppliedDefaultExpanded && this.expandedKeys.size === 0 && this.config.defaultExpanded !== false) {\n const allKeys = this.collectGroupKeys(groups);\n const initialExpanded = resolveDefaultExpanded(this.config.defaultExpanded ?? false, allKeys);\n if (initialExpanded.size > 0) {\n this.expandedKeys = new Set(initialExpanded);\n this.hasAppliedDefaultExpanded = true;\n }\n }\n\n const grouped = buildPreDefinedGroupModel({\n groups,\n expanded: this.expandedKeys,\n groupRows: this.groupRowsMap,\n loadingGroups: this.loadingGroups,\n });\n\n this.isActive = true;\n this.flattenedRows = grouped;\n this.computeFlatMeta();\n\n // Track visible data rows for animation\n this.keysToAnimate.clear();\n const currentVisibleKeys = new Set<string>();\n grouped.forEach((item, idx) => {\n if (item.kind === 'data') {\n const key = `data-${idx}`;\n currentVisibleKeys.add(key);\n if (!this.previousVisibleKeys.has(key)) {\n this.keysToAnimate.add(key);\n }\n }\n });\n this.previousVisibleKeys = currentVisibleKeys;\n\n // Return flattened rows for rendering\n return grouped.map((item) => {\n if (item.kind === 'group') {\n // Look up the pre-defined group to get rowCount from server\n const groupDef = this.findGroupDefinition(groups, item.key);\n const rowCount = groupDef?.rowCount ?? item.rows.length;\n return {\n __isGroupRow: true,\n __groupKey: item.key,\n __groupValue: item.value,\n __groupDepth: item.depth,\n __groupRows: item.rows,\n __groupExpanded: item.expanded,\n __groupRowCount: rowCount,\n __rowCacheKey: `group:${item.key}`,\n };\n }\n return item.row;\n });\n }\n\n /**\n * Collect all group keys from a pre-defined group tree.\n */\n private collectGroupKeys(groups: GroupDefinition[]): string[] {\n const keys: string[] = [];\n for (const g of groups) {\n keys.push(g.key);\n if (g.children?.length) {\n keys.push(...this.collectGroupKeys(g.children));\n }\n }\n return keys;\n }\n\n /**\n * Get the active group definitions (pre-defined or empty).\n */\n private getActiveGroups(): GroupDefinition[] {\n if (this.preDefinedGroups.length > 0) return this.preDefinedGroups;\n return Array.isArray(this.config.groups) ? this.config.groups : [];\n }\n\n /**\n * Find a group definition by key in the pre-defined group tree.\n */\n private findGroupDefinition(groups: GroupDefinition[], key: string): GroupDefinition | undefined {\n for (const g of groups) {\n if (g.key === key) return g;\n if (g.children?.length) {\n const found = this.findGroupDefinition(g.children, key);\n if (found) return found;\n }\n }\n return undefined;\n }\n // #endregion\n\n // #region Private Rendering Helpers\n\n /**\n * Create a toggle button for expanding/collapsing a group.\n */\n private createToggleButton(expanded: boolean, handleToggle: () => void): HTMLButtonElement {\n const btn = document.createElement('button');\n btn.type = 'button';\n btn.className = `${GridClasses.GROUP_TOGGLE}${expanded ? ` ${GridClasses.EXPANDED}` : ''}`;\n btn.setAttribute('aria-label', expanded ? 'Collapse group' : 'Expand group');\n this.setIcon(btn, expanded ? 'collapse' : 'expand');\n btn.addEventListener('click', (e) => {\n e.stopPropagation();\n handleToggle();\n });\n return btn;\n }\n\n /**\n * Get the formatted label text for a group.\n */\n private getGroupLabelText(value: unknown, depth: number, key: string): string {\n const config = this.config;\n return config.formatLabel ? config.formatLabel(value, depth, key) : String(value);\n }\n\n private renderFullWidthGroupRow(row: any, rowEl: HTMLElement, handleToggle: () => void): void {\n const config = this.config;\n const aggregators = config.aggregators ?? {};\n const groupRows = row.__groupRows ?? [];\n\n // Full-width mode: single spanning cell with toggle + label + count + aggregates\n const cell = document.createElement('div');\n cell.className = 'cell group-full';\n cell.style.gridColumn = '1 / -1';\n cell.setAttribute('role', 'gridcell');\n cell.setAttribute('data-col', '0'); // Required for focus/click delegation\n\n // Toggle button\n cell.appendChild(this.createToggleButton(row.__groupExpanded, handleToggle));\n\n // Group label\n const label = document.createElement('span');\n label.className = GridClasses.GROUP_LABEL;\n label.textContent = this.getGroupLabelText(row.__groupValue, row.__groupDepth || 0, row.__groupKey);\n cell.appendChild(label);\n\n // Row count\n if (config.showRowCount !== false) {\n const count = document.createElement('span');\n count.className = GridClasses.GROUP_COUNT;\n count.textContent = `(${row.__groupRowCount ?? row.__groupRows?.length ?? 0})`;\n cell.appendChild(count);\n }\n\n // Render aggregates if configured\n const aggregatorEntries = Object.entries(aggregators);\n if (aggregatorEntries.length > 0) {\n const aggregatesContainer = document.createElement('span');\n aggregatesContainer.className = 'group-aggregates';\n\n for (const [field, aggRef] of aggregatorEntries) {\n const col = this.columns.find((c) => c.field === field);\n const result = aggregatorRegistry.run(aggRef, groupRows, field, col);\n if (result != null) {\n const aggSpan = document.createElement('span');\n aggSpan.className = 'group-aggregate';\n aggSpan.setAttribute('data-field', field);\n // Use column header as label if available\n const colHeader = col?.header ?? field;\n aggSpan.textContent = `${colHeader}: ${result}`;\n aggregatesContainer.appendChild(aggSpan);\n }\n }\n\n if (aggregatesContainer.children.length > 0) {\n cell.appendChild(aggregatesContainer);\n }\n }\n\n rowEl.appendChild(cell);\n }\n\n private renderPerColumnGroupRow(row: any, rowEl: HTMLElement, handleToggle: () => void): void {\n const config = this.config;\n const aggregators = config.aggregators ?? {};\n const columns = this.columns;\n const groupRows = row.__groupRows ?? [];\n\n // Get grid template from the grid element\n const bodyEl = this.gridElement?.querySelector('.body') as HTMLElement | null;\n const gridTemplate = bodyEl?.style.gridTemplateColumns || '';\n if (gridTemplate) {\n rowEl.style.display = 'grid';\n rowEl.style.gridTemplateColumns = gridTemplate;\n }\n\n // Track whether we've rendered the toggle button yet (should be in first non-expander column)\n let toggleRendered = false;\n\n columns.forEach((col, colIdx) => {\n const cell = document.createElement('div');\n cell.className = 'cell group-cell';\n cell.setAttribute('data-col', String(colIdx));\n cell.setAttribute('role', 'gridcell');\n\n // Skip expander columns (they're handled by other plugins like MasterDetail/Tree)\n // but still render an empty cell to maintain grid structure\n if (isExpanderColumn(col)) {\n cell.setAttribute('data-field', col.field);\n rowEl.appendChild(cell);\n return;\n }\n\n // First non-expander column gets the toggle button + label\n if (!toggleRendered) {\n toggleRendered = true;\n cell.appendChild(this.createToggleButton(row.__groupExpanded, handleToggle));\n\n const label = document.createElement('span');\n const firstColAgg = aggregators[col.field];\n if (firstColAgg) {\n const aggResult = aggregatorRegistry.run(firstColAgg, groupRows, col.field, col);\n label.textContent = aggResult != null ? String(aggResult) : String(row.__groupValue);\n } else {\n label.textContent = this.getGroupLabelText(row.__groupValue, row.__groupDepth || 0, row.__groupKey);\n }\n cell.appendChild(label);\n\n if (config.showRowCount !== false) {\n const count = document.createElement('span');\n count.className = GridClasses.GROUP_COUNT;\n count.textContent = ` (${groupRows.length})`;\n cell.appendChild(count);\n }\n } else {\n // Other columns: run aggregator if defined\n const aggRef = aggregators[col.field];\n if (aggRef) {\n const result = aggregatorRegistry.run(aggRef, groupRows, col.field, col);\n cell.textContent = result != null ? String(result) : '';\n } else {\n cell.textContent = '';\n }\n }\n\n rowEl.appendChild(cell);\n });\n }\n // #endregion\n\n // #region Public API\n\n /**\n * Expand all groups.\n *\n * Safe to call immediately after {@link setGroupOn}: when the grouping\n * config has just changed, the cached `flattenedRows` is stale, so the\n * call defers until the next render rebuilds the group model against the\n * new `groupOn` (issue #335). The `group-toggle` event is emitted once\n * the new keys are known.\n */\n expandAll(): void {\n if (this.groupConfigDirty) {\n // flattenedRows is stale — defer until processRows rebuilds it. Clear\n // the cached set so callers don't observe stale keys via\n // `getExpandedGroups()` between this call and the next render.\n this.pendingExpansion = true;\n this.expandedKeys = new Set();\n this.requestRender();\n return;\n }\n this.expandedKeys = expandAllGroups(this.flattenedRows);\n this.broadcast<GroupToggleDetail>('group-toggle', { expandedKeys: [...this.expandedKeys] });\n this.requestRender();\n }\n\n /**\n * Collapse all groups.\n *\n * Safe to call immediately after {@link setGroupOn}: defers until the next\n * render rebuilds the group model so the collapse targets the new groups\n * rather than a stale snapshot (issue #335).\n */\n collapseAll(): void {\n if (this.groupConfigDirty) {\n this.pendingExpansion = false;\n this.expandedKeys = new Set();\n this.requestRender();\n return;\n }\n this.expandedKeys = collapseAllGroups();\n this.broadcast<GroupToggleDetail>('group-toggle', { expandedKeys: [...this.expandedKeys] });\n this.requestRender();\n }\n\n /**\n * Toggle expansion of a specific group.\n * In accordion mode, expanding a group will collapse all sibling groups.\n * @param key - The group key to toggle\n */\n toggle(key: string): void {\n const isExpanding = !this.expandedKeys.has(key);\n const config = this.config;\n\n // Find the group to get its depth for accordion mode\n const group = this.flattenedRows.find((r) => r.kind === 'group' && r.key === key) as GroupRowModelItem | undefined;\n\n // In accordion mode, collapse sibling groups when expanding\n if (config.accordion && isExpanding && group) {\n const newKeys = new Set<string>();\n // Keep only ancestors (keys that are prefixes of the current key) and the current key\n for (const existingKey of this.expandedKeys) {\n // Check if existingKey is an ancestor of the toggled key\n // Ancestors have composite keys that are prefixes of child keys (separated by '||')\n if (key.startsWith(existingKey + '||') || existingKey.startsWith(key + '||')) {\n // This is an ancestor or descendant - keep it only if ancestor\n if (key.startsWith(existingKey + '||')) {\n newKeys.add(existingKey);\n }\n } else {\n // Check depth - only keep groups at different depths\n const existingGroup = this.flattenedRows.find((r) => r.kind === 'group' && r.key === existingKey) as\n GroupRowModelItem | undefined;\n if (existingGroup && existingGroup.depth !== group.depth) {\n newKeys.add(existingKey);\n }\n }\n }\n newKeys.add(key);\n this.expandedKeys = newKeys;\n } else {\n this.expandedKeys = toggleGroupExpansion(this.expandedKeys, key);\n }\n\n this.broadcast<GroupToggleDetail>('group-toggle', {\n key,\n expanded: this.expandedKeys.has(key),\n value: group?.value,\n depth: group?.depth ?? 0,\n expandedKeys: [...this.expandedKeys],\n });\n\n // Emit group-expand/group-collapse events for pre-defined mode\n const activeGroups = this.getActiveGroups();\n if (activeGroups.length > 0) {\n const groupPath = getGroupPath(activeGroups, key);\n if (isExpanding) {\n this.emit<GroupExpandDetail>('group-expand', { groupKey: key, groupPath });\n\n // Request child rows via unified DataSource if not already cached\n if (!this.groupRowsMap.has(key)) {\n const groupDef = this.findGroupDefinition(activeGroups, key);\n if (groupDef) {\n this.loadingGroups.add(key);\n this.grid?.query?.('datasource:fetch-children', {\n context: { source: 'grouping-rows', groupKey: key, group: groupDef, groupPath },\n } satisfies FetchChildrenQuery);\n }\n }\n } else {\n this.emit<GroupCollapseDetail>('group-collapse', { groupKey: key, groupPath });\n }\n }\n\n // Announce group state change for screen readers\n const expanded = this.expandedKeys.has(key);\n const groupName = group?.value != null ? String(group.value) : key;\n if (expanded) {\n const rowCount = group?.rows?.length ?? 0;\n announce(this.gridElement, getA11yMessage(this.gridElement, 'groupExpanded', groupName, rowCount));\n } else {\n announce(this.gridElement, getA11yMessage(this.gridElement, 'groupCollapsed', groupName));\n }\n\n this.requestRender();\n }\n\n /**\n * Check if a specific group is expanded.\n * @param key - The group key to check\n * @returns Whether the group is expanded\n */\n isExpanded(key: string): boolean {\n return this.expandedKeys.has(key);\n }\n\n /**\n * Expand a specific group.\n * @param key - The group key to expand\n */\n expand(key: string): void {\n if (!this.expandedKeys.has(key)) {\n this.expandedKeys = new Set([...this.expandedKeys, key]);\n this.requestRender();\n }\n }\n\n /**\n * Collapse a specific group.\n * @param key - The group key to collapse\n */\n collapse(key: string): void {\n if (this.expandedKeys.has(key)) {\n const newKeys = new Set(this.expandedKeys);\n newKeys.delete(key);\n this.expandedKeys = newKeys;\n this.requestRender();\n }\n }\n\n /**\n * Get the current group state.\n * @returns Group state information\n */\n getGroupState(): GroupState {\n const groupRows = this.flattenedRows.filter((r) => r.kind === 'group');\n return {\n isActive: this.isActive,\n expandedCount: this.expandedKeys.size,\n totalGroups: groupRows.length,\n expandedKeys: [...this.expandedKeys],\n };\n }\n\n /**\n * Get the total count of visible rows (including group headers).\n * @returns Number of visible rows\n */\n getRowCount(): number {\n return this.flattenedRows.length;\n }\n\n /**\n * Refresh the grouped row model.\n * Call this after modifying groupOn or other config options.\n */\n refreshGroups(): void {\n this.requestRender();\n }\n\n /**\n * Get current expanded group keys.\n * @returns Array of expanded group keys\n */\n getExpandedGroups(): string[] {\n return [...this.expandedKeys];\n }\n\n /**\n * Get the flattened row model.\n * @returns Array of render rows (groups + data rows)\n */\n getFlattenedRows(): RenderRow[] {\n return this.flattenedRows;\n }\n\n /**\n * Check if grouping is currently active.\n * @returns Whether grouping is active\n */\n isGroupingActive(): boolean {\n return this.isActive;\n }\n\n /**\n * Set the groupOn function dynamically.\n *\n * Optionally specify the initial expansion state to apply against the new\n * group set. Without it, the previous `expandedKeys` are kept (and most\n * likely won't match the new keys, so groups land collapsed). With it,\n * the new groups are expanded synchronously on the next render — no need\n * to chain `setGroupOn(fn); expandAll()` or wait for `requestAnimationFrame`.\n *\n * If you do call `expandAll()` / `collapseAll()` separately right after\n * `setGroupOn`, those calls automatically defer to the same mechanism so\n * they target the new groups rather than the stale snapshot. See issue #335.\n *\n * @param fn - The groupOn function or undefined to disable\n * @param expanded - Optional initial expansion for the new grouping:\n * - `true`: expand all new groups\n * - `false`: collapse all new groups\n * - `string` / `string[]`: expand specific group key(s)\n * - `number`: expand the group at this index\n * When omitted, existing `expandedKeys` are preserved (legacy behavior).\n *\n * @example Group by a new field and expand everything\n * ```ts\n * plugin.setGroupOn((row) => row.counterparty, true);\n * ```\n *\n * @example Group by a new field but keep all groups collapsed\n * ```ts\n * plugin.setGroupOn((row) => row.region, false);\n * ```\n */\n setGroupOn(fn: ((row: any) => any[] | any | null | false) | undefined, expanded?: DefaultExpandedValue): void {\n (this.config as GroupingRowsConfig).groupOn = fn;\n this.groupConfigDirty = true;\n if (expanded !== undefined) {\n this.pendingExpansion = expanded;\n this.expandedKeys = new Set();\n this.hasAppliedDefaultExpanded = false;\n }\n this.requestRender();\n }\n\n // --- Pre-defined group API ---\n\n /**\n * Replace auto-detected groups with an externally provided group structure.\n *\n * When groups are set, the plugin switches to pre-defined mode — `groupOn`\n * is ignored and the plugin renders the provided group headers instead.\n * Row data for each group must be populated via {@link setGroupRows}.\n *\n * @param groups - Array of group definitions, or empty array to clear\n */\n setGroups(groups: GroupDefinition[]): void {\n this.preDefinedGroups = groups;\n this.groupRowsMap.clear();\n this.loadingGroups.clear();\n this.expandedKeys.clear();\n this.hasAppliedDefaultExpanded = false;\n this.requestRender();\n }\n\n /**\n * Get the current pre-defined group structure.\n *\n * Returns the groups set via {@link setGroups}, received from `datasource:data`,\n * or the `groups` config array. Returns an empty array when using `groupOn`-based grouping.\n *\n * @returns Current group definitions\n */\n getGroups(): GroupDefinition[] {\n if (this.preDefinedGroups.length > 0) return [...this.preDefinedGroups];\n return Array.isArray(this.config.groups) ? [...this.config.groups] : [];\n }\n\n /**\n * Populate row data for an expanded group.\n *\n * Call this in response to a `group-expand` event after fetching rows\n * from the server. The plugin will re-render to show the rows.\n *\n * When `ServerSidePlugin` is loaded, this is handled automatically via\n * `datasource:children` events — you only need this for the imperative API.\n *\n * @param groupKey - The group key to populate\n * @param rows - The row data for this group\n */\n setGroupRows(groupKey: string, rows: unknown[]): void {\n this.groupRowsMap.set(groupKey, rows);\n this.loadingGroups.delete(groupKey);\n this.requestRender();\n }\n\n /**\n * Toggle loading indicator for a group.\n *\n * When loading is true, the group shows a loading spinner instead of row data.\n * Call with `false` after rows are loaded (also cleared by {@link setGroupRows}).\n *\n * @param groupKey - The group key\n * @param loading - Whether the group is loading\n */\n setGroupLoading(groupKey: string, loading: boolean): void {\n if (loading) {\n this.loadingGroups.add(groupKey);\n } else {\n this.loadingGroups.delete(groupKey);\n }\n this.requestRender();\n }\n\n /**\n * Clear cached row data for one or all groups.\n *\n * Use when the server data has changed and groups need to be re-fetched\n * on next expand.\n *\n * @param groupKey - Specific group key to clear, or omit to clear all\n */\n clearGroupRows(groupKey?: string): void {\n if (groupKey != null) {\n this.groupRowsMap.delete(groupKey);\n } else {\n this.groupRowsMap.clear();\n }\n this.requestRender();\n }\n // #endregion\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../../../../../libs/grid/src/lib/core/constants.ts","../../../../../../libs/grid/src/lib/core/internal/value-accessor.ts","../../../../../../libs/grid/src/lib/core/internal/aggregators.ts","../../../../../../libs/grid/src/lib/core/defaults.ts","../../../../../../libs/grid/src/lib/core/internal/aria.ts","../../../../../../libs/grid/src/lib/core/internal/sanitize.ts","../../../../../../libs/grid/src/lib/core/internal/loading.ts","../../../../../../libs/grid/src/lib/core/internal/diagnostics.ts","../../../../../../libs/grid/src/lib/core/plugin/base-plugin.ts","../../../../../../libs/grid/src/lib/core/plugin/expander-column.ts","../../../../../../libs/grid/src/lib/plugins/grouping-rows/grouping-rows.ts","../../../../../../libs/grid/src/lib/plugins/grouping-rows/grouping-rows.css?inline","../../../../../../libs/grid/src/lib/plugins/grouping-rows/grouping-rows-plugin.ts"],"sourcesContent":["/**\n * Grid DOM Constants\n *\n * Centralized constants for CSS classes, data attributes, and selectors\n * used throughout the grid. Use these instead of magic strings.\n *\n * Note: Some constants here are used by plugins but defined in core because:\n * 1. The core CSS needs to style these elements (e.g., sticky positioning)\n * 2. Multiple plugins may share the same class names\n *\n * Plugins can define their own additional constants and export them.\n * See each plugin's index.ts for plugin-specific exports.\n */\n\n/**\n * CSS selector for focusable editor elements within a cell.\n * Used by EditingPlugin, UndoRedoPlugin, and keyboard navigation. Defined here\n * (a leaf module) rather than in `rows.ts` so `keyboard.ts` can consume it\n * without creating a rows ↔ keyboard import cycle.\n */\nexport const FOCUSABLE_EDITOR_SELECTOR =\n 'input,select,textarea,[contenteditable=\"true\"],[contenteditable=\"\"],[tabindex]:not([tabindex=\"-1\"])';\n\n// #region CSS Classes\n\n/**\n * CSS class names used in the grid's DOM.\n * Use these when adding/removing classes or querying elements.\n *\n * Classes are organized by:\n * - Core: Used by the grid core for structure and basic functionality\n * - Shared: Used by multiple features/plugins, styled by core CSS\n *\n * @category Plugin Development\n * @since 0.1.1\n */\nexport const GridClasses = {\n // ─── Core Structure ───────────────────────────────────────────────\n ROOT: 'tbw-grid-root',\n HEADER: 'header',\n HEADER_ROW: 'header-row',\n HEADER_CELL: 'header-cell',\n\n // Body structure\n ROWS_VIEWPORT: 'rows-viewport',\n ROWS_SPACER: 'rows-spacer',\n ROWS_CONTAINER: 'rows',\n\n // Row elements\n DATA_ROW: 'data-grid-row',\n GROUP_ROW: 'group-row',\n\n // Cell elements\n DATA_CELL: 'cell',\n\n // ─── Core States ──────────────────────────────────────────────────\n SELECTED: 'selected',\n FOCUSED: 'focused',\n EDITING: 'editing',\n EXPANDED: 'expanded',\n COLLAPSED: 'collapsed',\n DRAGGING: 'dragging',\n RESIZING: 'resizing',\n\n // Sorting (core feature)\n SORTABLE: 'sortable',\n SORTED_ASC: 'sorted-asc',\n SORTED_DESC: 'sorted-desc',\n\n // Visibility\n HIDDEN: 'hidden',\n\n // ─── Shared Classes (used by plugins, styled by core CSS) ────────\n // These are defined here because core CSS provides the base styling.\n // Plugins apply these classes; core CSS defines how they look.\n\n // Sticky positioning (PinnedColumnsPlugin applies, core CSS styles)\n STICKY_LEFT: 'sticky-left',\n STICKY_RIGHT: 'sticky-right',\n\n // Pinned rows (PinnedRowsPlugin applies, core CSS styles)\n PINNED_TOP: 'pinned-top',\n PINNED_BOTTOM: 'pinned-bottom',\n\n // Tree structure (TreePlugin applies, core CSS styles)\n TREE_TOGGLE: 'tree-toggle',\n TREE_INDENT: 'tree-indent',\n\n // Grouping (GroupingRowsPlugin applies, core CSS styles)\n GROUP_TOGGLE: 'group-toggle',\n GROUP_LABEL: 'group-label',\n GROUP_COUNT: 'group-count',\n\n // Selection (SelectionPlugin applies, core CSS styles)\n RANGE_SELECTION: 'range-selection',\n SELECTION_OVERLAY: 'selection-overlay',\n} as const;\n\n// #endregion\n\n// #region Data Attributes\n\n/**\n * Data attribute names used on grid elements.\n * Use these when getting/setting data attributes.\n *\n * @category Plugin Development\n * @since 0.1.1\n */\nexport const GridDataAttrs = {\n // ─── Core Attributes ──────────────────────────────────────────────\n FIELD: 'data-field',\n\n /**\n * Marks a `title` the grid itself put on a cell to reveal ellipsised text\n * (WCAG SC 1.4.12). Anything that sets its own `title` on a cell must clear\n * this, or the grid will later mistake that title for its own and replace it.\n *\n * @since 3.6.0\n */\n TRUNCATED: 'data-tbw-truncated',\n\n // ─── Shared Attributes (used by plugins) ──────────────────────────\n GROUP_KEY: 'data-group-key', // GroupingRowsPlugin\n} as const;\n\n// #endregion\n\n// #region Selectors\n\n/**\n * Common CSS selectors for querying grid elements.\n * Built from the class constants for consistency.\n *\n * @category Plugin Development\n * @since 0.1.1\n */\nexport const GridSelectors = {\n ROOT: `.${GridClasses.ROOT}`,\n HEADER: `.${GridClasses.HEADER}`,\n HEADER_ROW: `.${GridClasses.HEADER_ROW}`,\n HEADER_CELL: `.${GridClasses.HEADER_CELL}`,\n ROWS_VIEWPORT: `.${GridClasses.ROWS_VIEWPORT}`,\n ROWS_CONTAINER: `.${GridClasses.ROWS_CONTAINER}`,\n DATA_ROW: `.${GridClasses.DATA_ROW}`,\n DATA_CELL: `.${GridClasses.DATA_CELL}`,\n GROUP_ROW: `.${GridClasses.GROUP_ROW}`,\n\n // By data attribute\n CELL_BY_FIELD: (field: string) => `.${GridClasses.DATA_CELL}[${GridDataAttrs.FIELD}=\"${field}\"]`,\n\n // State selectors\n SELECTED_ROWS: `.${GridClasses.DATA_ROW}.${GridClasses.SELECTED}`,\n EDITING_CELL: `.${GridClasses.DATA_CELL}.${GridClasses.EDITING}`,\n} as const;\n\n// #endregion\n\n// #region CSS Custom Properties\n\n/**\n * CSS custom property names for theming.\n * Use these when programmatically setting styles.\n *\n * @category Plugin Development\n * @since 0.1.1\n */\nexport const GridCSSVars = {\n // Colors\n COLOR_BG: '--tbw-color-bg',\n COLOR_FG: '--tbw-color-fg',\n COLOR_FG_MUTED: '--tbw-color-fg-muted',\n COLOR_BORDER: '--tbw-color-border',\n COLOR_ACCENT: '--tbw-color-accent',\n COLOR_HEADER_BG: '--tbw-color-header-bg',\n COLOR_HEADER_FG: '--tbw-color-header-fg',\n COLOR_SELECTION: '--tbw-color-selection',\n COLOR_ROW_HOVER: '--tbw-color-row-hover',\n COLOR_ROW_ALT: '--tbw-color-row-alt',\n\n // Sizing\n ROW_HEIGHT: '--tbw-row-height',\n HEADER_HEIGHT: '--tbw-header-height',\n CELL_PADDING: '--tbw-cell-padding',\n\n // Typography\n FONT_FAMILY: '--tbw-font-family',\n FONT_SIZE: '--tbw-font-size',\n\n // Borders\n BORDER_RADIUS: '--tbw-border-radius',\n FOCUS_OUTLINE: '--tbw-focus-outline',\n} as const;\n\n// #endregion\n\n// Type helpers\n\n/**\n * Union of all CSS class names used in the grid's DOM.\n * Use `GridClasses` to access individual values by key.\n *\n * @category Plugin Development\n * @since 0.1.1\n */\nexport type GridClassName = (typeof GridClasses)[keyof typeof GridClasses];\n\n/**\n * Union of all `data-*` attribute names used on grid elements.\n * Use `GridDataAttrs` to access individual values by key.\n *\n * @category Plugin Development\n * @since 0.1.1\n */\nexport type GridDataAttr = (typeof GridDataAttrs)[keyof typeof GridDataAttrs];\n\n/**\n * Union of all CSS custom property names (`--tbw-*`) available for theming.\n * Use `GridCSSVars` to access individual values by key.\n *\n * @category Plugin Development\n * @since 0.1.1\n */\nexport type GridCSSVar = (typeof GridCSSVars)[keyof typeof GridCSSVars];\n","/**\n * Value Accessor Module\n *\n * Single source of truth for resolving a column's value from a row.\n *\n * Resolution precedence:\n * 1. `column.valueAccessor({ row, column, rowIndex })` — when defined\n * 2. `row[column.field]` — direct field read (default)\n *\n * Per-column escape hatches (`sortComparator`, `filterValue`) still take\n * precedence over the accessor — the accessor is the *default* value source,\n * never an override. See {@link BaseColumnConfig.valueAccessor}.\n *\n * Performance: results are memoized per (row identity, column field) using a\n * `WeakMap`. Row identity changes (immutable updates) invalidate naturally;\n * in-place mutations must call {@link invalidateAccessorCache} on the row.\n */\n\nimport type { ColumnConfig } from '../types';\n\n// Box-wrapped values let the cache-hit path do a SINGLE `Map.get` instead of\n// `has()` + `get()`. A truthy box means cache hit (even when the cached value\n// itself is `undefined` / `null` / `0` / `''`).\ninterface CacheBox {\n v: unknown;\n}\nconst accessorCache = new WeakMap<object, Map<string, CacheBox>>();\n\n// #region Nested dotted-path field access (issue #438)\n\n// Prototype-pollution guard applied to every path segment on read AND write.\n// Uses explicit `===` comparisons (not a Set lookup) so static analyzers\n// recognize the barrier guarding the assignments in `setByPath`/`writeCellField`.\nfunction isUnsafeKey(key: string): boolean {\n return key === '__proto__' || key === 'constructor' || key === 'prototype';\n}\n\n// Parsed dotted-path cache: field string → segments. Only **dotted** fields are\n// cached; a plain key is recognised by a single `indexOf` scan, which is cheaper\n// than hashing the string for a Map probe and — critically — keeps the cache\n// from growing without bound when a plugin mints synthetic field names at\n// runtime (PivotPlugin derives one column per distinct pivot value).\nconst fieldPathCache = new Map<string, readonly string[]>();\n\n/**\n * Parse a column `field` into path segments, or `null` when it is a plain\n * top-level key (contains no `.`). Dotted results are memoized.\n *\n * @param field - The column field key (may be a dotted path like `a.b.c`).\n * @returns Frozen segment array for dotted paths, else `null`.\n * @since 3.3.0\n */\nexport function parseFieldPath(field: string): readonly string[] | null {\n if (field.indexOf('.') === -1) return null;\n let segments = fieldPathCache.get(field);\n if (segments === undefined) {\n segments = Object.freeze(field.split('.'));\n fieldPathCache.set(field, segments);\n }\n return segments;\n}\n\n/**\n * Read a nested value from `row` by pre-parsed path segments. Returns\n * `undefined` on any nullish/non-object hop. Prototype-pollution safe.\n *\n * @since 3.3.0\n */\nexport function getByPath(row: unknown, path: readonly string[]): unknown {\n let cur = row as Record<string, unknown> | null | undefined;\n for (let i = 0; i < path.length; i++) {\n if (cur == null || typeof cur !== 'object') return undefined;\n const seg = path[i];\n if (isUnsafeKey(seg)) return undefined;\n cur = cur[seg] as Record<string, unknown>;\n }\n return cur;\n}\n\n/**\n * Write `value` into `row` at the location described by pre-parsed path\n * segments. Walks **existing** objects only — it does NOT fabricate missing\n * intermediates — and returns `true` only when the write happened.\n * Prototype-pollution safe (every segment is guarded).\n *\n * @since 3.3.0\n */\nexport function setByPath(row: unknown, path: readonly string[], value: unknown): boolean {\n if (row == null || typeof row !== 'object') return false;\n let cur = row as Record<string, unknown>;\n for (let i = 0; i < path.length - 1; i++) {\n const seg = path[i];\n if (isUnsafeKey(seg)) return false;\n const next = cur[seg];\n if (next == null || typeof next !== 'object') return false;\n cur = next as Record<string, unknown>;\n }\n const last = path[path.length - 1];\n if (isUnsafeKey(last)) return false;\n cur[last] = value;\n return true;\n}\n\n/**\n * Read the raw stored value for `field` from `row`, honoring nested dotted\n * paths — the non-accessor counterpart to {@link resolveCellValue}.\n *\n * Resolution rules (back-compat critical, see `grid-core.md` DECIDED #438):\n * 1. Plain field (no `.`) → direct `row[field]` (identical to legacy path),\n * except prototype-polluting keys (`__proto__`/`constructor`/`prototype`)\n * which return `undefined` — symmetric with {@link writeCellField}.\n * 2. Dotted field whose literal key is an **own property** of `row` → that\n * flat value wins (preserves synthetic-key rows that really store `'a.b'`).\n * 3. Otherwise → nested traversal via {@link getByPath}.\n *\n * @since 3.3.0\n */\nexport function readCellField(row: unknown, field: string): unknown {\n if (row == null) return undefined;\n const path = parseFieldPath(field);\n const r = row as Record<string, unknown>;\n if (path === null) return isUnsafeKey(field) ? undefined : r[field];\n if (Object.prototype.hasOwnProperty.call(r, field)) return r[field];\n return getByPath(r, path);\n}\n\n/**\n * A precompiled reader that extracts one column's raw value from a row.\n *\n * @since 3.4.0\n */\nexport type FieldReader = (row: unknown) => unknown;\n\n/**\n * Build a {@link FieldReader} for `field`, resolving the plain / unsafe /\n * dotted decision **once** instead of on every cell read.\n *\n * Hoist the reader out of any per-row loop (sort comparators, compiled filter\n * predicates, aggregators, export writers). Semantics are identical to\n * {@link readCellField}; only the dispatch is paid up front.\n *\n * @param field - The column field key (may be a dotted path like `a.b.c`).\n * @returns A single-argument reader closure.\n *\n * @example\n * ```typescript\n * const read = createFieldReader(column.field);\n * rows.sort((a, b) => compare(read(a), read(b)));\n * ```\n * @since 3.4.0\n */\nexport function createFieldReader(field: string): FieldReader {\n const path = parseFieldPath(field);\n if (path === null) {\n if (isUnsafeKey(field)) return () => undefined;\n return (row) => (row == null ? undefined : (row as Record<string, unknown>)[field]);\n }\n return (row) => {\n if (row == null) return undefined;\n const r = row as Record<string, unknown>;\n // A literal own key containing a dot wins over traversal (see readCellField).\n if (Object.prototype.hasOwnProperty.call(r, field)) return r[field];\n return getByPath(r, path);\n };\n}\n\n/**\n * Whether `field` is a plain, safe top-level key — i.e. `row[field]` is exactly\n * equivalent to {@link readCellField} and callers may inline the property read.\n *\n * Used by benchmarked hot loops (compiled filter predicates) that keep a\n * hand-inlined fast path and only fall back to a {@link FieldReader} for the\n * dotted/unsafe minority.\n *\n * @since 3.4.0\n */\nexport function isPlainField(field: string): boolean {\n return field.indexOf('.') === -1 && !isUnsafeKey(field);\n}\n\n/**\n * Write `value` into `row` for `field`, honoring nested dotted paths and the\n * same back-compat precedence as {@link readCellField}. Returns `true` when a\n * write occurred. Prototype-pollution safe.\n *\n * @since 3.3.0\n */\nexport function writeCellField(row: unknown, field: string, value: unknown): boolean {\n if (row == null || typeof row !== 'object') return false;\n const path = parseFieldPath(field);\n const r = row as Record<string, unknown>;\n if (path === null) {\n if (isUnsafeKey(field)) return false;\n r[field] = value;\n return true;\n }\n // A literal own key containing a dot wins over traversal (symmetric with\n // readCellField). Such a key always contains `.`, so it can never be an\n // unsafe prototype key.\n if (Object.prototype.hasOwnProperty.call(r, field)) {\n r[field] = value;\n return true;\n }\n return setByPath(r, path, value);\n}\n\n// #endregion\n\n/**\n * Resolve the cell value for a column, honoring `valueAccessor` if defined,\n * otherwise reading `row[column.field]` (with nested dotted-path support).\n *\n * @param row - The row object\n * @param column - The column definition\n * @param rowIndex - The visible row index (passed to accessor; defaults to -1)\n * @returns The resolved cell value\n *\n * @example\n * ```typescript\n * const value = resolveCellValue(row, column);\n * ```\n * @since 2.2.0\n */\nexport function resolveCellValue<TRow>(row: TRow, column: ColumnConfig<TRow>, rowIndex = -1): unknown {\n if (!column.valueAccessor) {\n return readCellField(row, column.field);\n }\n // Non-object rows (primitives, null) bypass the cache.\n if (typeof row !== 'object' || row === null) {\n return column.valueAccessor({ row, column, rowIndex });\n }\n // After the guard above, `row` is provably a non-null object. Single\n // assertion (no `as unknown as`) bridges the generic `TRow` to the WeakMap\n // key type; see `.github/instructions/typescript-conventions.instructions.md`.\n const rowKey = row as object;\n const key = column.field;\n let cellMap = accessorCache.get(rowKey);\n if (cellMap) {\n const box = cellMap.get(key);\n if (box !== undefined) return box.v;\n } else {\n cellMap = new Map();\n accessorCache.set(rowKey, cellMap);\n }\n const value = column.valueAccessor({ row, column, rowIndex });\n cellMap.set(key, { v: value });\n return value;\n}\n\n/**\n * Invalidate cached accessor values for a row (after in-place mutation),\n * a single (row, field) pair, or — when called with no argument — clear\n * the entire cache. Immutable updates auto-invalidate via row identity.\n *\n * Edit/transaction paths that mutate row objects in-place must call this.\n * @since 2.2.0\n */\nexport function invalidateAccessorCache(row?: object, field?: string): void {\n if (!row) {\n // WeakMap can't be cleared in O(1); the GC handles unreferenced rows.\n // For an explicit full reset, callers should mutate row identities instead.\n return;\n }\n if (!field) {\n accessorCache.delete(row);\n return;\n }\n accessorCache.get(row)?.delete(field);\n}\n\n/**\n * Build a `(value, row) => unknown` extractor compatible with\n * `filterValue`-style APIs from a column's `valueAccessor`. Returns `undefined`\n * when the column has no accessor.\n *\n * Used by FilteringPlugin to bridge the two API shapes without duplicating\n * cache lookups.\n */\nexport function accessorAsFilterValue<TRow>(\n column: ColumnConfig<TRow>,\n): ((value: unknown, row: TRow) => unknown) | undefined {\n if (!column.valueAccessor) return undefined;\n return (_value, row) => resolveCellValue(row, column);\n}\n","/**\n * Aggregators Core Registry\n *\n * Provides a central registry for aggregator functions.\n * Built-in aggregators are provided by default.\n * Plugins can register additional aggregators.\n *\n * The registry is exposed as a singleton object that can be accessed:\n * - By ES module imports: import { aggregatorRegistry } from '@toolbox-web/grid'\n * - By UMD/CDN: TbwGrid.aggregatorRegistry\n * - By plugins via context: ctx.aggregatorRegistry\n */\n\n/* eslint-disable @typescript-eslint/no-explicit-any */\n\nimport { readCellField, resolveCellValue } from './value-accessor';\n\n/**\n * Aggregation function as stored in the core registry.\n *\n * Loosely typed (`any`) on purpose: the registry holds aggregators contributed\n * by any plugin for any row shape. `plugins/pinned-rows/types.ts` declares a\n * same-named but *stricter* public alias (`unknown[]` / `ColumnConfig`) for\n * users authoring an aggregator. The two must stay separate — tightening this\n * one breaks registry storage, loosening that one loses user-facing type\n * safety.\n */\nexport type AggregatorFn = (rows: any[], field: string, column?: any) => any;\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 plugin-registered aggregator name\n * - A custom {@link AggregatorFn}\n *\n * Aggregators are not declared on a column directly — they are configured per\n * field on the consumer (e.g. `RowGroupRenderConfig.aggregators` for group\n * rows, keyed by column field name).\n *\n * @example\n * ```typescript\n * // Built-in aggregator on a group row config\n * { aggregators: { amount: 'sum' } }\n *\n * // Custom aggregator function\n * {\n * aggregators: {\n * price: (rows, field) => {\n * const values = rows.map((r) => (r as any)[field]).filter((v) => v != null);\n * return values.length ? Math.max(...(values as number[])) : null;\n * },\n * },\n * }\n * ```\n *\n * @since 0.1.1\n */\nexport type AggregatorRef = string | AggregatorFn;\n\n/**\n * Read a cell for an aggregator. When the column has a `valueAccessor`,\n * route through it (single source of truth — see issue #230); otherwise\n * fall back to a direct field read for the fast path.\n */\nfunction readCell(row: any, field: string, column?: any): unknown {\n if (column?.valueAccessor) return resolveCellValue(row, column);\n return readCellField(row, field);\n}\n\n/**\n * Check whether a cell value should be treated as blank and skipped by numeric\n * aggregators. Matches the semantics used by the filter engine:\n * `null` / `undefined` / `''` / `NaN` are all considered absent values.\n *\n * Without this guard, `Number('')` coerces to `0` and `null` coerces to `0`,\n * which silently pulls blank cells into sums, averages, and min/max comparisons\n * (a blank cell would otherwise \"win\" a min/max against any positive/negative\n * dataset). Skipping blanks mirrors how Excel treats empty cells in SUM/AVG/MIN/MAX.\n */\nfunction isBlankCell(v: unknown): boolean {\n return v == null || v === '' || (typeof v === 'number' && isNaN(v));\n}\n\n/** Built-in aggregator functions */\nconst builtInAggregators: Record<string, AggregatorFn> = {\n sum: (rows, field, column) => {\n let sum = 0;\n for (let i = 0; i < rows.length; i++) {\n const raw = readCell(rows[i], field, column);\n if (isBlankCell(raw)) continue;\n const n = Number(raw);\n if (!isNaN(n)) sum += n;\n }\n return sum;\n },\n avg: (rows, field, column) => {\n if (!rows.length) return 0;\n let sum = 0;\n let count = 0;\n for (let i = 0; i < rows.length; i++) {\n const raw = readCell(rows[i], field, column);\n if (isBlankCell(raw)) continue;\n const n = Number(raw);\n if (isNaN(n)) continue;\n sum += n;\n count++;\n }\n return count > 0 ? sum / count : 0;\n },\n count: (rows) => rows.length,\n min: (rows, field, column) => {\n if (!rows.length) return 0;\n let min = Infinity;\n for (let i = 0; i < rows.length; i++) {\n const raw = readCell(rows[i], field, column);\n if (isBlankCell(raw)) continue;\n const v = Number(raw);\n if (isNaN(v)) continue;\n if (v < min) min = v;\n }\n return min === Infinity ? 0 : min;\n },\n max: (rows, field, column) => {\n if (!rows.length) return 0;\n let max = -Infinity;\n for (let i = 0; i < rows.length; i++) {\n const raw = readCell(rows[i], field, column);\n if (isBlankCell(raw)) continue;\n const v = Number(raw);\n if (isNaN(v)) continue;\n if (v > max) max = v;\n }\n return max === -Infinity ? 0 : max;\n },\n first: (rows, field, column) => (rows[0] ? readCell(rows[0], field, column) : undefined),\n last: (rows, field, column) => (rows.length ? readCell(rows[rows.length - 1], field, column) : undefined),\n};\n\n/** Custom aggregator registry (for plugins to add to) */\nconst customAggregators: Map<string, AggregatorFn> = new Map();\n\n/**\n * The aggregator registry singleton.\n * Plugins should access this through context or the global namespace.\n */\nexport const aggregatorRegistry = {\n /**\n * Register a custom aggregator function.\n */\n register(name: string, fn: AggregatorFn): void {\n customAggregators.set(name, fn);\n },\n\n /**\n * Unregister a custom aggregator function.\n */\n unregister(name: string): void {\n customAggregators.delete(name);\n },\n\n /**\n * Get an aggregator function by reference.\n */\n get(ref: AggregatorRef | undefined): AggregatorFn | undefined {\n if (ref === undefined) return undefined;\n if (typeof ref === 'function') return ref;\n // Check custom first, then built-in\n return customAggregators.get(ref) ?? builtInAggregators[ref];\n },\n\n /**\n * Run an aggregator on a set of rows.\n */\n run(ref: AggregatorRef | undefined, rows: any[], field: string, column?: any): any {\n const fn = this.get(ref);\n return fn ? fn(rows, field, column) : undefined;\n },\n\n /**\n * Check if an aggregator exists.\n */\n has(name: string): boolean {\n return customAggregators.has(name) || name in builtInAggregators;\n },\n\n /**\n * List all available aggregator names.\n */\n list(): string[] {\n return [...Object.keys(builtInAggregators), ...customAggregators.keys()];\n },\n};\n\n// #region Value-based Aggregators\n// Used by plugins like Pivot that work with pre-extracted numeric values\n\nexport type ValueAggregatorFn = (values: number[]) => number;\n\n/**\n * Built-in value-based aggregators.\n * These operate on arrays of numbers (unlike row-based aggregators).\n */\nconst builtInValueAggregators: Record<string, ValueAggregatorFn> = {\n sum: (vals) => {\n let sum = 0;\n for (let i = 0; i < vals.length; i++) sum += vals[i];\n return sum;\n },\n avg: (vals) => {\n if (!vals.length) return 0;\n let sum = 0;\n for (let i = 0; i < vals.length; i++) sum += vals[i];\n return sum / vals.length;\n },\n count: (vals) => vals.length,\n min: (vals) => {\n if (!vals.length) return 0;\n let min = vals[0];\n for (let i = 1; i < vals.length; i++) {\n if (vals[i] < min) min = vals[i];\n }\n return min;\n },\n max: (vals) => {\n if (!vals.length) return 0;\n let max = vals[0];\n for (let i = 1; i < vals.length; i++) {\n if (vals[i] > max) max = vals[i];\n }\n return max;\n },\n first: (vals) => vals[0] ?? 0,\n last: (vals) => vals[vals.length - 1] ?? 0,\n};\n\n/**\n * Get a value-based aggregator function.\n * Used by Pivot plugin and other features that aggregate pre-extracted values.\n *\n * @param aggFunc - Aggregation function name ('sum', 'avg', 'count', 'min', 'max', 'first', 'last')\n * @returns Aggregator function that takes number[] and returns number\n */\nexport function getValueAggregator(aggFunc: string): ValueAggregatorFn {\n return builtInValueAggregators[aggFunc] ?? builtInValueAggregators.sum;\n}\n\n/**\n * Run a value-based aggregator on a set of values.\n *\n * @param aggFunc - Aggregation function name\n * @param values - Array of numbers to aggregate\n * @returns Aggregated result\n */\nexport function runValueAggregator(aggFunc: string, values: number[]): number {\n return getValueAggregator(aggFunc)(values);\n}\n// #endregion\n","/**\n * Runtime default values for grid configuration.\n *\n * Kept out of `core/types.ts` so that file stays type-only apart from the two\n * enum-like consts whose `type` aliases are derived from them via `typeof`.\n * Import types from `./types`; import the values that back them from here.\n */\n\nimport type { A11yMessages, AnimationConfig, GridIcons } from './types';\n\n/**\n * Default English announcement messages.\n * Used when no custom messages are provided via {@link A11yConfig.messages}.\n * @since 2.0.0\n */\nexport const DEFAULT_A11Y_MESSAGES: Required<A11yMessages> = {\n sortApplied: (column, direction) => `Sorted by ${column}, ${direction}`,\n sortCleared: () => 'Sort cleared',\n filterApplied: (column) => `Filter applied on ${column}`,\n filterCleared: (column) => `Filter cleared from ${column}`,\n allFiltersCleared: () => 'All filters cleared',\n groupExpanded: (name, count) => `Group ${name} expanded, ${count} rows`,\n groupCollapsed: (name) => `Group ${name} collapsed`,\n selectionChanged: (count) => `${count} rows selected`,\n selectAllRows: () => 'Select all rows',\n selectRow: (rowIndex) => `Select row ${rowIndex + 1}`,\n columnSelected: (label) => `Column ${label} selected`,\n columnSelectionChanged: (count) => `${count} columns selected`,\n columnSelectionCleared: () => 'Column selection cleared',\n selectionAxisChanged: (toAxis) =>\n toAxis === 'column'\n ? 'Row selection cleared, column selection active'\n : 'Column selection cleared, row selection active',\n rangeSpanSelected: (axis, count) =>\n count === 1 ? `${axis === 'row' ? 'Row' : 'Column'} selected` : `${count} ${axis}s selected`,\n editingStarted: (rowIndex) => `Editing row ${rowIndex + 1}`,\n editingCommitted: (rowIndex) => `Row ${rowIndex + 1} saved`,\n dataLoaded: (count) => `${count} rows loaded`,\n};\n\n/**\n * Default animation configuration\n * @since 0.2.7\n */\nexport const DEFAULT_ANIMATION_CONFIG: Required<Omit<AnimationConfig, 'sort'>> = {\n mode: 'reduced-motion',\n duration: 200,\n easing: 'ease-out',\n};\n\n/**\n * Default icons used when not overridden. Most entries are short text/emoji;\n * `filter` and `filterActive` are empty strings because the actual rendering\n * is driven by the `--tbw-icon-filter[-active]-mask` CSS custom properties\n * (see `core/styles/variables.css`). Userland that wants an HTML/SVG fallback\n * via `gridConfig.icons.filter = '<svg…>'` is unaffected — that path overrides this default.\n * @since 0.1.1\n */\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: '',\n filterActive: '',\n print: '🖨️',\n};\n","/**\n * ARIA Accessibility Helpers\n *\n * Pure functions for managing ARIA attributes on grid elements.\n * Implements caching to avoid redundant DOM writes during scroll.\n *\n * @module internal/aria\n */\n\nimport { DEFAULT_A11Y_MESSAGES } from '../defaults';\nimport type { A11yMessages, GridConfig } from '../types';\n\n// #region Types\n\n/**\n * State for caching ARIA attributes to avoid redundant DOM writes.\n */\nexport interface AriaState {\n /** Last set row count */\n rowCount: number;\n /** Last set column count */\n colCount: number;\n /** Last set aria-label */\n ariaLabel: string | undefined;\n /** Last set aria-labelledby */\n ariaLabelledBy: string | undefined;\n /** Last set aria-describedby */\n ariaDescribedBy: string | undefined;\n /** Last set aria-roledescription */\n ariaRoleDescription: string | undefined;\n /** Last source row count announced via `dataLoaded`; used to suppress duplicate announcements */\n lastAnnouncedSourceCount: number;\n}\n\n/**\n * Create initial ARIA state.\n */\nexport function createAriaState(): AriaState {\n return {\n rowCount: -1,\n colCount: -1,\n ariaLabel: undefined,\n ariaLabelledBy: undefined,\n ariaDescribedBy: undefined,\n ariaRoleDescription: undefined,\n lastAnnouncedSourceCount: -1,\n };\n}\n\n// #endregion\n\n// #region Count Updates\n\n/**\n * Update ARIA row and column counts on grid elements.\n * Uses caching to avoid redundant DOM writes on every scroll frame.\n *\n * @param state - ARIA state for caching\n * @param rowsBodyEl - Element to set aria-rowcount/aria-colcount on\n * @param bodyEl - Element to set role=\"rowgroup\" on\n * @param rowCount - Current row count\n * @param colCount - Current column count\n * @returns true if anything was updated\n */\nexport function updateAriaCounts(\n state: AriaState,\n rowsBodyEl: HTMLElement | null,\n bodyEl: HTMLElement | null,\n rowCount: number,\n colCount: number,\n): boolean {\n // Skip if nothing changed (hot path optimization for scroll)\n if (rowCount === state.rowCount && colCount === state.colCount) {\n return false;\n }\n\n const prevRowCount = state.rowCount;\n state.rowCount = rowCount;\n state.colCount = colCount;\n\n // Update ARIA counts on inner grid element\n if (rowsBodyEl) {\n rowsBodyEl.setAttribute('aria-rowcount', String(rowCount));\n rowsBodyEl.setAttribute('aria-colcount', String(colCount));\n }\n\n // Set role=\"rowgroup\" on .rows only when there are rows (ARIA compliance)\n if (rowCount !== prevRowCount && bodyEl) {\n if (rowCount > 0) {\n bodyEl.setAttribute('role', 'rowgroup');\n } else {\n bodyEl.removeAttribute('role');\n }\n }\n\n return true;\n}\n\n// #endregion\n\n// #region Label Updates\n\n/**\n * Determine the effective aria-label for the grid.\n * Priority: explicit config > shell title > nothing\n *\n * The shell title is read from `config.shell.header.title`. The shell plugin\n * folds any light-DOM `<tbw-grid-header title>` into that field via its\n * `processConfig` hook (extraction #370), so core needs no plugin state here.\n *\n * @param config - Grid configuration (the merged effective config)\n * @returns The aria-label to use, or undefined\n */\nexport function getEffectiveAriaLabel<T>(config: GridConfig<T> | undefined): string | undefined {\n const explicitLabel = config?.gridAriaLabel;\n if (explicitLabel) return explicitLabel;\n\n return config?.shell?.header?.title ?? undefined;\n}\n\n/**\n * Update ARIA label and describedby attributes on the grid container.\n * Uses caching to avoid redundant DOM writes.\n *\n * @param state - ARIA state for caching\n * @param rowsBodyEl - Element to set aria-label/aria-describedby on\n * @param config - Grid configuration (the merged effective config)\n * @returns true if anything was updated\n */\nexport function updateAriaLabels<T>(\n state: AriaState,\n rowsBodyEl: HTMLElement | null,\n config: GridConfig<T> | undefined,\n): boolean {\n if (!rowsBodyEl) return false;\n\n let updated = false;\n\n // aria-labelledby wins per WAI-ARIA accessible-name precedence — when set,\n // `aria-label` is suppressed even if explicitly configured to avoid two\n // competing names being computed by AT.\n const ariaLabelledBy = config?.gridAriaLabelledBy;\n const effectiveLabel = ariaLabelledBy ? undefined : getEffectiveAriaLabel(config);\n\n // Update aria-labelledby only if changed\n if (ariaLabelledBy !== state.ariaLabelledBy) {\n state.ariaLabelledBy = ariaLabelledBy;\n if (ariaLabelledBy) {\n rowsBodyEl.setAttribute('aria-labelledby', ariaLabelledBy);\n } else {\n rowsBodyEl.removeAttribute('aria-labelledby');\n }\n updated = true;\n }\n\n // Update aria-label only if changed\n if (effectiveLabel !== state.ariaLabel) {\n state.ariaLabel = effectiveLabel;\n if (effectiveLabel) {\n rowsBodyEl.setAttribute('aria-label', effectiveLabel);\n } else {\n rowsBodyEl.removeAttribute('aria-label');\n }\n updated = true;\n }\n\n // Update aria-describedby only if changed\n const ariaDescribedBy = config?.gridAriaDescribedBy;\n if (ariaDescribedBy !== state.ariaDescribedBy) {\n state.ariaDescribedBy = ariaDescribedBy;\n if (ariaDescribedBy) {\n rowsBodyEl.setAttribute('aria-describedby', ariaDescribedBy);\n } else {\n rowsBodyEl.removeAttribute('aria-describedby');\n }\n updated = true;\n }\n\n // Update aria-roledescription only if changed. Apps can override the\n // announced role name (e.g. localized \"Tabell\" instead of generic \"grid\").\n const ariaRoleDescription = config?.gridAriaRoleDescription;\n if (ariaRoleDescription !== state.ariaRoleDescription) {\n state.ariaRoleDescription = ariaRoleDescription;\n if (ariaRoleDescription) {\n rowsBodyEl.setAttribute('aria-roledescription', ariaRoleDescription);\n } else {\n rowsBodyEl.removeAttribute('aria-roledescription');\n }\n updated = true;\n }\n\n return updated;\n}\n\n// #endregion\n\n// #region Live Announcements\n\n/**\n * Announce a message to screen readers via the grid's aria-live region.\n * Clears then sets text to ensure repeated messages are re-announced.\n *\n * Respects `effectiveConfig.a11y.announcements` — if set to `false`,\n * the announcement is silently skipped.\n *\n * @param gridEl - The grid host element (or any ancestor containing `.tbw-sr-only`)\n * @param message - The message to announce\n */\nexport function announce(gridEl: HTMLElement, message: string): void {\n // Check if announcements are disabled via config.\n // gridEl is always a DataGridElement at runtime — use property access safely.\n if (!gridEl) return;\n const config =\n 'effectiveConfig' in gridEl\n ? (gridEl as HTMLElement & { effectiveConfig?: GridConfig }).effectiveConfig\n : undefined;\n if (config?.a11y?.announcements === false) return;\n\n const el = gridEl.querySelector?.('.tbw-sr-only');\n if (!el) return;\n // Clear first so identical consecutive messages are still announced\n el.textContent = '';\n // `requestAnimationFrame` can be undefined if a debounced announcement fires\n // after the DOM/host environment is torn down (notably in unit tests). Fall\n // back to a direct assignment so `announce` never throws out of band.\n if (typeof requestAnimationFrame === 'function') {\n requestAnimationFrame(() => {\n el.textContent = message;\n });\n } else {\n el.textContent = message;\n }\n}\n\n/**\n * Get an announcement message, preferring custom overrides from config.\n * Falls back to default English messages when no override is provided.\n *\n * @param gridEl - The grid host element\n * @param key - The message key from A11yMessages\n * @param args - Arguments to pass to the message function\n */\nexport function getA11yMessage<K extends keyof A11yMessages>(\n gridEl: HTMLElement,\n key: K,\n ...args: Parameters<Required<A11yMessages>[K]>\n): string {\n type MessageFn = (...a: Parameters<Required<A11yMessages>[K]>) => string;\n const config =\n gridEl && 'effectiveConfig' in gridEl\n ? (gridEl as HTMLElement & { effectiveConfig?: GridConfig }).effectiveConfig\n : undefined;\n const customFn = config?.a11y?.messages?.[key] as MessageFn | undefined;\n if (customFn) return customFn(...args);\n return (DEFAULT_A11Y_MESSAGES[key] as MessageFn)(...args);\n}\n\n/**\n * Announce a `dataLoaded` message — but only when the source row count has\n * actually changed since the last announcement. This prevents announcement\n * spam from internal `data-change` emits triggered by sort/filter/edit, which\n * have their own dedicated announcements.\n *\n * @param gridEl - The grid host element\n * @param state - ARIA state (tracks last announced count)\n * @param sourceRowCount - Current source row count\n */\nexport function announceDataLoaded(gridEl: HTMLElement, state: AriaState, sourceRowCount: number): void {\n if (sourceRowCount === state.lastAnnouncedSourceCount) return;\n const previousAnnouncedSourceCount = state.lastAnnouncedSourceCount;\n state.lastAnnouncedSourceCount = sourceRowCount;\n // Skip only the initial sentinel → 0 transition (empty grid mount). A later\n // transition to 0 (e.g. data cleared / reloaded to an empty result set) should\n // still announce so screen-reader users hear that the grid is now empty.\n if (sourceRowCount === 0 && previousAnnouncedSourceCount < 0) return;\n announce(gridEl, getA11yMessage(gridEl, 'dataLoaded', sourceRowCount));\n}\n\n// #endregion\n","// Centralized template expression evaluation & sanitization utilities.\n// Responsible for safely interpolating {{ }} expressions while blocking\n// access to dangerous globals / reflective capabilities.\nimport type { CompiledViewFunction, EvalContext } from '../types';\n\n// #region Constants\nconst EXPR_RE = /{{\\s*([^}]+)\\s*}}/g;\nconst EMPTY_SENTINEL = '__DG_EMPTY__';\nconst SAFE_EXPR = /^[\\w$. '?+\\-*/%:()!<>=,&|]+$/;\n\n// Forbidden identifiers built at runtime to avoid static analysis scanners\n// from flagging string literals (e.g. socket.dev \"network access\" / \"uses eval\").\n// Stored as reversed, pipe-delimited tokens — reversed back when constructing the regex.\nconst _F =\n '__otorp__|__retteGenifed__|__retteSenifed__|rotcurtsnoc|wodniw|sihTlabolg|labolg|ssecorp|noitcnuF|tropmi|lave|tcelfeR|yxorP|rorrE|stnemugra|tnemucod|noitacol|eikooc|egarotSlacol|egarotSnoisses|BDdexedni|hctef|tseuqeRpttHLMX|tekcoSbeW|rekroW|rekroWderahS|rekroWecivreS|renepo|tnerap|pot|semarf|fles'\n .split('|')\n .map((s) => s.split('').reverse().join(''));\nconst FORBIDDEN = new RegExp(`__(proto|defineGetter|defineSetter)|${_F.slice(3).join('|')}|this\\\\b`);\n// #endregion\n\n// #region HTML Sanitization\n\n/**\n * Escape a plain text string for safe insertion into HTML.\n * Converts special HTML characters to their entity equivalents.\n *\n * @param text - Plain text string to escape\n * @returns HTML-safe string\n */\nexport function escapeHtml(text: string): string {\n if (!text || typeof text !== 'string') return '';\n return text\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;')\n .replace(/'/g, '&#39;');\n}\n\n/**\n * Tags that are considered dangerous and will be completely removed.\n * These can execute scripts, load external resources, or manipulate the page.\n */\nconst DANGEROUS_TAGS = new Set(\n 'script|iframe|object|embed|form|input|button|textarea|select|link|meta|base|style|template|slot|portal|frame|frameset|applet|noscript|noembed|plaintext|xmp|listing|math'.split(\n '|',\n ),\n);\n\n/**\n * Attributes that are considered dangerous - event handlers and data loading.\n */\nconst DANGEROUS_ATTR_PATTERN = /^on\\w+$/i;\n\n/**\n * Attributes that can contain URLs which might be javascript: or data: URIs.\n */\nconst URL_ATTRS = new Set('href|src|action|formaction|data|srcdoc|xlink:href|poster|srcset'.split('|'));\n\n/**\n * Protocol patterns that are dangerous in URLs.\n */\nconst DANGEROUS_URL_PROTOCOL = /^\\s*(javascript|vbscript|data|blob):/i;\n\n/**\n * Sanitize an HTML string by removing dangerous tags and attributes.\n *\n * Prefer {@link setSanitizedHTML} wherever the result is destined for an\n * element: this function has to re-serialize the sanitized tree back to a\n * string, and the caller's `innerHTML =` then re-parses it. That\n * parse→serialize→parse round trip is the classic mutation-XSS vector, and it\n * costs a second parse. Use this overload only when a string is genuinely\n * required (e.g. the value is stored, compared, or handed to another API).\n *\n * @param html - Raw HTML string to sanitize\n * @returns Sanitized HTML string safe for innerHTML\n */\nexport function sanitizeHTML(html: string): string {\n if (!html || typeof html !== 'string') return '';\n\n // Fast path: if no HTML tags at all, return as-is (already safe)\n if (html.indexOf('<') === -1) return html;\n\n const template = document.createElement('template');\n template.innerHTML = html;\n\n sanitizeNode(template.content);\n\n return template.innerHTML;\n}\n\n/**\n * Parse and sanitize an HTML string into a detached {@link DocumentFragment}.\n *\n * This is the primitive {@link setSanitizedHTML} builds on, exposed for callers\n * that need to inspect or relocate the nodes before insertion.\n *\n * @param html - Raw HTML string to sanitize\n * @returns Fragment containing the sanitized nodes (empty for falsy input)\n */\nexport function sanitizeToFragment(html: string): DocumentFragment {\n const template = document.createElement('template');\n if (html && typeof html === 'string') {\n template.innerHTML = html;\n sanitizeNode(template.content);\n }\n return template.content;\n}\n\n/**\n * Replace an element's children with the sanitized parse of `html`.\n *\n * **This is the canonical sink for user-supplied markup** — cell/header/group\n * renderer string returns, `gridConfig.icons.*`, light-DOM tool panel fallback\n * content, and any new extension point that turns a user string into markup.\n * `textContent` is wrong here (it renders SVG icon strings as literal text) and\n * raw `innerHTML` is wrong (it is an XSS sink).\n *\n * Nodes are moved from the parse fragment rather than re-serialized, so the\n * sanitized tree is exactly what lands in the document.\n *\n * @param el - Element whose children are replaced\n * @param html - Raw HTML string to sanitize and insert\n */\nexport function setSanitizedHTML(el: Element, html: string): void {\n if (!html || typeof html !== 'string') {\n el.replaceChildren();\n return;\n }\n // Nothing for the parser to do: no markup and no entities to decode.\n if (html.indexOf('<') === -1 && html.indexOf('&') === -1) {\n el.textContent = html;\n return;\n }\n el.replaceChildren(sanitizeToFragment(html));\n}\n\n/**\n * Recursively sanitize a DOM node tree.\n */\nfunction sanitizeNode(root: DocumentFragment | Element): void {\n const toRemove: Element[] = [];\n\n // Use querySelectorAll to find all elements, then filter\n const elements = root.querySelectorAll('*');\n\n for (const el of elements) {\n const tagName = el.tagName.toLowerCase();\n\n // Check if tag is dangerous\n if (DANGEROUS_TAGS.has(tagName)) {\n toRemove.push(el);\n continue;\n }\n\n // SVG elements need special handling - they can contain script-like behavior\n if (tagName === 'svg' || el.namespaceURI === 'http://www.w3.org/2000/svg') {\n // Remove entire SVG if it has any suspicious attributes\n const hasDangerousContent = Array.from(el.attributes).some(\n (attr) => DANGEROUS_ATTR_PATTERN.test(attr.name) || attr.name === 'href' || attr.name === 'xlink:href',\n );\n if (hasDangerousContent) {\n toRemove.push(el);\n continue;\n }\n }\n\n // Check and remove dangerous attributes\n const attrsToRemove: string[] = [];\n for (const attr of el.attributes) {\n const attrName = attr.name.toLowerCase();\n\n // Event handlers (onclick, onerror, onload, etc.)\n if (DANGEROUS_ATTR_PATTERN.test(attrName)) {\n attrsToRemove.push(attr.name);\n continue;\n }\n\n // `is=` upgrades a plain tag into a registered customized built-in, so it\n // can smuggle behaviour past the tag-name check above.\n if (attrName === 'is') {\n attrsToRemove.push(attr.name);\n continue;\n }\n\n // URL attributes with dangerous protocols\n if (URL_ATTRS.has(attrName) && DANGEROUS_URL_PROTOCOL.test(attr.value)) {\n attrsToRemove.push(attr.name);\n continue;\n }\n\n // style attribute can contain expressions (IE) or url() with javascript:\n if (attrName === 'style' && /expression\\s*\\(|javascript:|behavior\\s*:/i.test(attr.value)) {\n attrsToRemove.push(attr.name);\n continue;\n }\n }\n\n attrsToRemove.forEach((name) => el.removeAttribute(name));\n }\n\n // Remove dangerous elements (do this after iteration to avoid modifying during traversal)\n toRemove.forEach((el) => el.remove());\n}\n\n// #endregion\n\n// #region Template Evaluation\n\n// #region Safe Expression Evaluator\n// A minimal recursive-descent evaluator for the token set allowed by SAFE_EXPR.\n// Replaces `new Function()` to avoid dynamic code execution warnings from\n// static analysis tools (socket.dev, npm audit) while maintaining identical\n// functionality for expressions that pass the allowlist guards.\n//\n// Supported grammar (matching SAFE_EXPR = /^[\\w$. '?+\\-*/%:()!<>=,&|]+$/):\n// ternary → logicalOr ('?' ternary ':' ternary)?\n// logicalOr → logicalAnd ('||' logicalAnd)*\n// logicalAnd → equality ('&&' equality)*\n// equality → comparison (('==' | '!=' | '===' | '!==') comparison)*\n// comparison → additive (('<' | '>' | '<=' | '>=') additive)*\n// additive → multiplicative (('+' | '-') multiplicative)*\n// multiplicative → unary (('*' | '/' | '%') unary)*\n// unary → '!' unary | primary\n// primary → NUMBER | STRING | IDENT ('.' IDENT)? | '(' ternary ')'\n\ntype Token =\n { type: 'num'; v: number } | { type: 'str'; v: string } | { type: 'id'; v: string } | { type: 'op'; v: string };\n\nfunction tokenize(expr: string): Token[] | null {\n const tokens: Token[] = [];\n let i = 0;\n while (i < expr.length) {\n const ch = expr[i];\n if (ch === ' ') {\n i++;\n continue;\n }\n\n // Numbers\n if ((ch >= '0' && ch <= '9') || (ch === '.' && i + 1 < expr.length && expr[i + 1] >= '0' && expr[i + 1] <= '9')) {\n const start = i;\n while (i < expr.length && ((expr[i] >= '0' && expr[i] <= '9') || expr[i] === '.')) i++;\n tokens.push({ type: 'num', v: Number(expr.slice(start, i)) });\n continue;\n }\n\n // String literals (single-quoted only, per SAFE_EXPR)\n if (ch === \"'\") {\n let s = '';\n i++; // skip opening quote\n while (i < expr.length && expr[i] !== \"'\") s += expr[i++];\n if (i >= expr.length) return null; // unterminated\n i++; // skip closing quote\n tokens.push({ type: 'str', v: s });\n continue;\n }\n\n // Identifiers (value, row, row.field)\n if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || ch === '_' || ch === '$') {\n const start = i;\n while (i < expr.length && /[\\w$.]/.test(expr[i])) i++;\n tokens.push({ type: 'id', v: expr.slice(start, i) });\n continue;\n }\n\n // Multi-char operators\n if (i + 2 < expr.length) {\n const tri = expr.slice(i, i + 3);\n if (tri === '===' || tri === '!==') {\n tokens.push({ type: 'op', v: tri });\n i += 3;\n continue;\n }\n }\n if (i + 1 < expr.length) {\n const bi = expr.slice(i, i + 2);\n if (bi === '==' || bi === '!=' || bi === '<=' || bi === '>=' || bi === '&&' || bi === '||') {\n tokens.push({ type: 'op', v: bi });\n i += 2;\n continue;\n }\n }\n\n // Single-char operators\n if ('+-*/%<>!?:(),'.includes(ch)) {\n tokens.push({ type: 'op', v: ch });\n i++;\n continue;\n }\n\n return null; // unexpected character\n }\n return tokens;\n}\n\nfunction safeEval(expr: string, ctx: EvalContext): unknown {\n const toks = tokenize(expr);\n if (!toks) return undefined;\n const tokens: Token[] = toks;\n let pos = 0;\n\n function peek(): Token | undefined {\n return tokens[pos];\n }\n function advance(): Token {\n return tokens[pos++];\n }\n function match(type: string, val?: string): boolean {\n const t = peek();\n if (!t) return false;\n if (t.type !== type) return false;\n if (val !== undefined && t.v !== val) return false;\n return true;\n }\n\n function resolveId(name: string): unknown {\n if (name === 'value') return ctx.value;\n if (name.startsWith('row.')) {\n const key = name.slice(4);\n return ctx.row ? ctx.row[key] : undefined;\n }\n if (name.startsWith('typeDefault.')) {\n const key = name.slice('typeDefault.'.length);\n return ctx.typeDefault ? ctx.typeDefault[key] : undefined;\n }\n // bare 'row' reference\n if (name === 'row') return ctx.row;\n if (name === 'typeDefault') return ctx.typeDefault;\n return undefined;\n }\n\n function parseTernary(): unknown {\n const result = parseLogicalOr();\n if (match('op', '?')) {\n advance(); // consume '?'\n const consequent = parseTernary();\n if (!match('op', ':')) return undefined;\n advance(); // consume ':'\n const alternate = parseTernary();\n return result ? consequent : alternate;\n }\n return result;\n }\n\n function parseLogicalOr(): unknown {\n let left = parseLogicalAnd();\n while (match('op', '||')) {\n advance();\n left = left || parseLogicalAnd();\n }\n return left;\n }\n\n function parseLogicalAnd(): unknown {\n let left = parseEquality();\n while (match('op', '&&')) {\n advance();\n left = left && parseEquality();\n }\n return left;\n }\n\n function parseEquality(): unknown {\n let left = parseComparison();\n while (true) {\n const t = peek();\n if (!t || t.type !== 'op') break;\n if (t.v === '===') {\n advance();\n left = left === parseComparison();\n } else if (t.v === '!==') {\n advance();\n left = left !== parseComparison();\n } else if (t.v === '==') {\n advance();\n left = left == parseComparison();\n } else if (t.v === '!=') {\n advance();\n left = left != parseComparison();\n } else break;\n }\n return left;\n }\n\n function parseComparison(): unknown {\n let left = parseAdditive();\n while (true) {\n const t = peek();\n if (!t || t.type !== 'op') break;\n if (t.v === '<') {\n advance();\n left = (left as number) < (parseAdditive() as number);\n } else if (t.v === '>') {\n advance();\n left = (left as number) > (parseAdditive() as number);\n } else if (t.v === '<=') {\n advance();\n left = (left as number) <= (parseAdditive() as number);\n } else if (t.v === '>=') {\n advance();\n left = (left as number) >= (parseAdditive() as number);\n } else break;\n }\n return left;\n }\n\n function parseAdditive(): unknown {\n let left = parseMultiplicative();\n while (true) {\n const t = peek();\n if (!t || t.type !== 'op') break;\n if (t.v === '+') {\n advance();\n left = (left as number) + (parseMultiplicative() as number);\n } else if (t.v === '-') {\n advance();\n left = (left as number) - (parseMultiplicative() as number);\n } else break;\n }\n return left;\n }\n\n function parseMultiplicative(): unknown {\n let left = parseUnary();\n while (true) {\n const t = peek();\n if (!t || t.type !== 'op') break;\n if (t.v === '*') {\n advance();\n left = (left as number) * (parseUnary() as number);\n } else if (t.v === '/') {\n advance();\n left = (left as number) / (parseUnary() as number);\n } else if (t.v === '%') {\n advance();\n left = (left as number) % (parseUnary() as number);\n } else break;\n }\n return left;\n }\n\n function parseUnary(): unknown {\n if (match('op', '!')) {\n advance();\n return !parseUnary();\n }\n return parsePrimary();\n }\n\n function parsePrimary(): unknown {\n const t = peek();\n if (!t) return undefined;\n if (t.type === 'num') {\n advance();\n return t.v;\n }\n if (t.type === 'str') {\n advance();\n return t.v;\n }\n if (t.type === 'id') {\n advance();\n return resolveId(t.v as string);\n }\n if (t.type === 'op' && t.v === '(') {\n advance(); // consume '('\n const result = parseTernary();\n if (match('op', ')')) advance(); // consume ')'\n return result;\n }\n // Unary minus for negative numbers\n if (t.type === 'op' && t.v === '-') {\n advance();\n return -(parsePrimary() as number);\n }\n return undefined;\n }\n\n const result = parseTernary();\n // Ensure all tokens were consumed\n if (pos < tokens.length) return undefined;\n return result;\n}\n// #endregion\n\nexport function evalTemplateString(raw: string, ctx: EvalContext): string {\n if (!raw || raw.indexOf('{{') === -1) return raw; // fast path (no expressions)\n const parts: { expr: string; result: string }[] = [];\n const evaluated = raw.replace(EXPR_RE, (_m, expr) => {\n const res = evalSingle(expr, ctx);\n parts.push({ expr: expr.trim(), result: res });\n // The template markup is author-controlled; the interpolated data is not.\n // Escaping here stops row data from injecting elements or breaking out of\n // an attribute (sanitizeHTML downstream only strips *dangerous* markup).\n return res === EMPTY_SENTINEL ? res : escapeHtml(res);\n });\n const finalStr = postProcess(evaluated);\n // If every part evaluated to EMPTY_SENTINEL we treat this as intentionally blank.\n // If any expression was blocked due to forbidden token (EMPTY_SENTINEL) we *still* only output ''\n // but do not escalate to BLOCKED_SENTINEL unless the original contained explicit forbidden tokens.\n const allEmpty = parts.length && parts.every((p) => p.result === '' || p.result === EMPTY_SENTINEL);\n const hadForbidden = REFLECTIVE_RE.test(raw);\n if (hadForbidden || allEmpty) return '';\n return finalStr;\n}\n\nfunction evalSingle(expr: string, ctx: EvalContext): string {\n expr = (expr || '').trim();\n if (!expr) return EMPTY_SENTINEL;\n if (REFLECTIVE_RE.test(expr)) return EMPTY_SENTINEL;\n if (expr === 'value') return ctx.value == null ? EMPTY_SENTINEL : String(ctx.value);\n if (expr.startsWith('row.') && !/[()?]/.test(expr) && !expr.includes(':')) {\n const key = expr.slice(4);\n const v = ctx.row ? ctx.row[key] : undefined;\n return v == null ? EMPTY_SENTINEL : String(v);\n }\n if (expr.startsWith('typeDefault.') && !/[()?]/.test(expr) && !expr.includes(':')) {\n const key = expr.slice('typeDefault.'.length);\n const v = ctx.typeDefault ? ctx.typeDefault[key] : undefined;\n return v == null ? EMPTY_SENTINEL : String(v);\n }\n if (expr.length > 80) return EMPTY_SENTINEL;\n if (!SAFE_EXPR.test(expr) || FORBIDDEN.test(expr)) return EMPTY_SENTINEL;\n const dotChain = expr.match(/\\./g);\n if (dotChain && dotChain.length > 1) return EMPTY_SENTINEL;\n try {\n const out = safeEval(expr, ctx);\n const str = out == null ? '' : String(out);\n if (REFLECTIVE_RE.test(str)) return EMPTY_SENTINEL;\n return str || EMPTY_SENTINEL;\n } catch {\n return EMPTY_SENTINEL;\n }\n}\n// #endregion\n\n// #region Cell Scrubbing\n/** Pattern matching reflective/introspective APIs that must be stripped from output. */\nconst REFLECTIVE_RE = /Reflect|Proxy|ownKeys/;\n\nfunction postProcess(s: string): string {\n if (!s) return s;\n return s.replace(new RegExp(EMPTY_SENTINEL, 'g'), '').replace(/Reflect\\.[^<>{}\\s]+|\\bProxy\\b|ownKeys\\([^)]*\\)/g, '');\n}\n\nexport function finalCellScrub(cell: HTMLElement): void {\n if (!REFLECTIVE_RE.test(cell.textContent || '')) return;\n // First pass: clear only text nodes containing forbidden tokens\n for (const n of cell.childNodes) {\n if (n.nodeType === Node.TEXT_NODE && REFLECTIVE_RE.test(n.textContent || '')) n.textContent = '';\n }\n // If forbidden tokens persist in element nodes, nuke everything\n if (REFLECTIVE_RE.test(cell.textContent || '')) {\n cell.textContent = '';\n }\n}\n// #endregion\n\n// #region Template Compilation\nexport function compileTemplate(raw: string) {\n if (REFLECTIVE_RE.test(raw)) {\n const fn = (() => '') as CompiledViewFunction;\n fn.__blocked = true;\n return fn;\n }\n const fn = ((ctx: EvalContext) => evalTemplateString(raw, ctx)) as CompiledViewFunction;\n fn.__blocked = false;\n return fn;\n}\n// #endregion\n","/**\n * Loading State Module\n *\n * Handles loading overlays, row loading, and cell loading states.\n * Provides DOM manipulation helpers for loading indicators.\n *\n * @module internal/loading\n */\n\nimport type { GridConfig, LoadingContext } from '../types';\nimport { setSanitizedHTML } from './sanitize';\n\n/**\n * Create the default spinner element.\n * @param size - 'large' for grid overlay, 'small' for row/cell\n */\nexport function createDefaultSpinner(size: 'large' | 'small'): HTMLElement {\n const spinner = document.createElement('div');\n spinner.className = `tbw-spinner tbw-spinner--${size}`;\n spinner.setAttribute('role', 'progressbar');\n spinner.setAttribute('aria-label', 'Loading');\n return spinner;\n}\n\n/**\n * Create loading content using custom renderer or default spinner.\n *\n * Strings returned by a custom renderer are passed through `setSanitizedHTML()`\n * — mirroring the cell, header and empty-state render paths — so a message\n * interpolating server data (e.g. `Loading ${dataset.name}…`) is not an XSS\n * sink.\n *\n * @param size - 'large' for grid overlay, 'small' for row/cell\n * @param renderer - Optional custom loading renderer from config\n */\nexport function createLoadingContent(size: 'large' | 'small', renderer?: GridConfig['loadingRenderer']): HTMLElement {\n if (renderer) {\n const context: LoadingContext = { size };\n const result = renderer(context);\n if (typeof result === 'string') {\n const wrapper = document.createElement('div');\n setSanitizedHTML(wrapper, result);\n return wrapper;\n }\n return result;\n }\n\n return createDefaultSpinner(size);\n}\n\n/**\n * Create or update the loading overlay element.\n * @param renderer - Optional custom loading renderer from config\n */\nexport function createLoadingOverlay(renderer?: GridConfig['loadingRenderer']): HTMLElement {\n const overlay = document.createElement('div');\n overlay.className = 'tbw-loading-overlay';\n overlay.setAttribute('role', 'status');\n overlay.setAttribute('aria-live', 'polite');\n overlay.appendChild(createLoadingContent('large', renderer));\n return overlay;\n}\n\n/**\n * Show the loading overlay on the grid root element.\n * @param gridRoot - The .tbw-grid-root element\n * @param overlayEl - The overlay element (will be cached by caller)\n */\nexport function showLoadingOverlay(gridRoot: Element, overlayEl: HTMLElement): void {\n gridRoot.appendChild(overlayEl);\n}\n\n/**\n * Hide the loading overlay.\n * @param overlayEl - The overlay element to remove\n */\nexport function hideLoadingOverlay(overlayEl: HTMLElement | undefined): void {\n overlayEl?.remove();\n}\n\n/**\n * Update a row element's loading state.\n * Uses real DOM elements instead of ::before/::after pseudo-elements\n * because the selection plugin already uses ::after on rows for border styling.\n * @param rowEl - The row element\n * @param loading - Whether the row is loading\n */\nexport function setRowLoadingState(rowEl: HTMLElement, loading: boolean): void {\n if (loading) {\n rowEl.classList.add('tbw-row-loading');\n rowEl.setAttribute('aria-busy', 'true');\n\n // Create overlay + spinner DOM elements if not already present\n if (!rowEl.querySelector('.tbw-row-loading-overlay')) {\n const overlay = document.createElement('div');\n overlay.className = 'tbw-row-loading-overlay';\n overlay.setAttribute('aria-hidden', 'true');\n\n const spinner = document.createElement('div');\n spinner.className = 'tbw-row-loading-spinner';\n overlay.appendChild(spinner);\n\n rowEl.appendChild(overlay);\n }\n } else {\n rowEl.classList.remove('tbw-row-loading');\n rowEl.removeAttribute('aria-busy');\n\n // Remove overlay + spinner DOM elements\n rowEl.querySelector('.tbw-row-loading-overlay')?.remove();\n }\n}\n\n/**\n * Update a cell element's loading state.\n * @param cellEl - The cell element\n * @param loading - Whether the cell is loading\n */\nexport function setCellLoadingState(cellEl: HTMLElement, loading: boolean): void {\n if (loading) {\n cellEl.classList.add('tbw-cell-loading');\n cellEl.setAttribute('aria-busy', 'true');\n } else {\n cellEl.classList.remove('tbw-cell-loading');\n cellEl.removeAttribute('aria-busy');\n }\n}\n","/**\n * Centralized diagnostic messages for @toolbox-web/grid.\n *\n * Every user-facing warning, error, or info message in the grid has a unique\n * diagnostic code (e.g. `TBW001`). Each code maps to a section on the online\n * troubleshooting page, giving developers a direct link to resolution steps.\n *\n * ## Usage\n *\n * ```ts\n * import { MISSING_BREAKPOINT, warnDiagnostic, throwDiagnostic } from './diagnostics';\n *\n * // Warn with a code\n * warnDiagnostic(MISSING_BREAKPOINT, 'Set a breakpoint...', gridId);\n *\n * // Throw with a code\n * throwDiagnostic(MISSING_ROW_ID, 'Configure getRowId...', gridId);\n * ```\n *\n * Plugins should prefer `this.warn(MISSING_BREAKPOINT, message)` via BaseGridPlugin\n * instead of importing this module directly.\n *\n * @internal\n */\n\n// #region Grid Prefix\n\n/**\n * Build the `[tbw-grid]` or `[tbw-grid#my-id]` log prefix.\n * Pass `pluginName` for a scoped prefix like `[tbw-grid:SelectionPlugin]`.\n */\nexport function gridPrefix(gridId?: string, pluginName?: string): string {\n const id = gridId ? `#${gridId}` : '';\n const plugin = pluginName ? `:${pluginName}` : '';\n return `[tbw-grid${id}${plugin}]`;\n}\n\n// #endregion\n\n// #region Diagnostic Codes\n\n/**\n * Diagnostic codes used across the grid library.\n *\n * Each code is an individual export so that unused codes are tree-shaken\n * from bundles that don't reference them (esbuild can't tree-shake\n * properties from a single object).\n *\n * Naming: TBW + 3-digit number.\n * Ranges:\n * 001–019 Configuration validation (missing plugins, bad config)\n * 020–029 Plugin lifecycle (dependencies, incompatibilities, alias collapse)\n * 030–039 Feature registry\n * 040–049 Row operations (row ID, row mutations)\n * 050–059 Column operations (width, template)\n * 060–069 Rendering (callbacks, formatters, external views)\n * 070–079 Shell (tool panels, header/toolbar content)\n * 080–089 Editing & editors\n * 090–099 Print\n * 100–109 Clipboard\n * 110–119 Plugin-specific (responsive, undo-redo, grouping-columns)\n * 120–129 Style injection\n * 130–139 Attribute parsing\n * 140–149 DataSource / ServerSide\n */\n\n// --- Config validation (001–019) ---\n/** Column uses a plugin-owned property but the plugin is not loaded. */\nexport const MISSING_PLUGIN = 'TBW001' as const;\n/** Grid config uses a plugin-owned property but the plugin is not loaded. */\nexport const MISSING_PLUGIN_CONFIG = 'TBW002' as const;\n/** Plugin config rule violation (error severity). */\nexport const CONFIG_RULE_ERROR = 'TBW003' as const;\n/** Plugin config rule violation (warning severity). */\nexport const CONFIG_RULE_WARN = 'TBW004' as const;\n\n// --- Plugin lifecycle (020–029) ---\n/** Required plugin dependency is missing. */\nexport const MISSING_DEPENDENCY = 'TBW020' as const;\n/** Optional plugin dependency is missing. */\nexport const OPTIONAL_DEPENDENCY = 'TBW021' as const;\n/** Two loaded plugins are incompatible. */\nexport const INCOMPATIBLE_PLUGINS = 'TBW022' as const;\n/** Two plugin instances resolve to the same canonical name (alias collapse). */\nexport const PLUGIN_ALIAS_COLLAPSE = 'TBW023' as const;\n/** Error thrown inside a plugin event handler. */\nexport const PLUGIN_EVENT_ERROR = 'TBW024' as const;\n/** Conflicting config values found while merging alias-collapsed plugin instances. */\nexport const PLUGIN_ALIAS_CONFIG_CONFLICT = 'TBW025' as const;\n\n// --- Feature registry (030–039) ---\n/** Feature was re-registered (overwritten). */\nexport const FEATURE_REREGISTERED = 'TBW030' as const;\n/** Feature configured but not imported. */\nexport const FEATURE_NOT_IMPORTED = 'TBW031' as const;\n/** Feature depends on another feature that is not enabled. */\nexport const FEATURE_MISSING_DEP = 'TBW032' as const;\n\n// --- Row operations (040–049) ---\n/** Cannot determine row ID (no getRowId and no id property). */\nexport const MISSING_ROW_ID = 'TBW040' as const;\n/** Row with given ID not found. */\nexport const ROW_NOT_FOUND = 'TBW041' as const;\n\n// --- Column operations (050–059) ---\n/** Column has an invalid CSS width value. */\nexport const INVALID_COLUMN_WIDTH = 'TBW050' as const;\n\n// --- Rendering callbacks (060–069) ---\n/** rowClass callback threw an error. */\nexport const ROW_CLASS_ERROR = 'TBW060' as const;\n/** cellClass callback threw an error. */\nexport const CELL_CLASS_ERROR = 'TBW061' as const;\n/** Column format function threw an error. */\nexport const FORMAT_ERROR = 'TBW062' as const;\n/** External view mount() threw an error. */\nexport const VIEW_MOUNT_ERROR = 'TBW063' as const;\n/** External view event dispatch error. */\nexport const VIEW_DISPATCH_ERROR = 'TBW064' as const;\n\n// --- Shell (070–079) ---\n/** Tool panel missing required id. */\nexport const TOOL_PANEL_MISSING_ATTR = 'TBW070' as const;\n/** No tool panels registered. */\nexport const NO_TOOL_PANELS = 'TBW071' as const;\n/** Tool panel section not found. */\nexport const TOOL_PANEL_NOT_FOUND = 'TBW072' as const;\n/** Tool panel already registered. */\nexport const TOOL_PANEL_DUPLICATE = 'TBW073' as const;\n/** Header content already registered. */\nexport const HEADER_CONTENT_DUPLICATE = 'TBW074' as const;\n/** Toolbar content already registered. */\nexport const TOOLBAR_CONTENT_DUPLICATE = 'TBW075' as const;\n\n// --- Editing & editors (080–089) ---\n/** External editor mount() threw an error. */\nexport const EDITOR_MOUNT_ERROR = 'TBW080' as const;\n\n// --- Print (090–099) ---\n/** Print already in progress. */\nexport const PRINT_IN_PROGRESS = 'TBW090' as const;\n/** Grid not available for printing. */\nexport const PRINT_NO_GRID = 'TBW091' as const;\n/** Print operation failed. */\nexport const PRINT_FAILED = 'TBW092' as const;\n/** Multiple elements share the same grid ID (print isolation issue). */\nexport const PRINT_DUPLICATE_ID = 'TBW093' as const;\n\n// --- Clipboard (100–109) ---\n/** Clipboard API write failed. */\nexport const CLIPBOARD_FAILED = 'TBW100' as const;\n\n// --- Plugin-specific (110–119) ---\n/** ResponsivePlugin: no breakpoint configured. */\nexport const MISSING_BREAKPOINT = 'TBW110' as const;\n/** UndoRedoPlugin: transaction already in progress. */\nexport const TRANSACTION_IN_PROGRESS = 'TBW111' as const;\n/** UndoRedoPlugin: no transaction in progress. */\nexport const NO_TRANSACTION = 'TBW112' as const;\n/** GroupingColumnsPlugin: missing id or header on column group definition. */\nexport const COLUMN_GROUP_NO_ID = 'TBW113' as const;\n/** GroupingColumnsPlugin: conflicting columnGroups sources. */\nexport const COLUMN_GROUPS_CONFLICT = 'TBW114' as const;\n\n// --- Style injection (120–129) ---\n/** Failed to extract grid.css from document stylesheets. */\nexport const STYLE_EXTRACT_FAILED = 'TBW120' as const;\n/** Could not find grid.css in document.styleSheets. */\nexport const STYLE_NOT_FOUND = 'TBW121' as const;\n\n// --- Attribute parsing (130–139) ---\n/** Invalid JSON in an HTML attribute. */\nexport const INVALID_ATTRIBUTE_JSON = 'TBW130' as const;\n\n// --- DataSource / ServerSide (140–149) ---\n/** ServerSidePlugin: getRows() rejected. */\nexport const DATASOURCE_FETCH_ERROR = 'TBW140' as const;\n/** ServerSidePlugin: getChildRows() rejected. */\nexport const DATASOURCE_CHILD_FETCH_ERROR = 'TBW141' as const;\n/** ServerSidePlugin: getChildRows() not implemented but a plugin queried for children. */\nexport const DATASOURCE_NO_CHILD_HANDLER = 'TBW142' as const;\n/** ServerSidePlugin: concurrent request limit reached, request deferred. */\nexport const DATASOURCE_THROTTLED = 'TBW143' as const;\n\nexport type DiagnosticCode =\n | typeof MISSING_PLUGIN\n | typeof MISSING_PLUGIN_CONFIG\n | typeof CONFIG_RULE_ERROR\n | typeof CONFIG_RULE_WARN\n | typeof MISSING_DEPENDENCY\n | typeof OPTIONAL_DEPENDENCY\n | typeof INCOMPATIBLE_PLUGINS\n | typeof PLUGIN_ALIAS_COLLAPSE\n | typeof PLUGIN_EVENT_ERROR\n | typeof PLUGIN_ALIAS_CONFIG_CONFLICT\n | typeof FEATURE_REREGISTERED\n | typeof FEATURE_NOT_IMPORTED\n | typeof FEATURE_MISSING_DEP\n | typeof MISSING_ROW_ID\n | typeof ROW_NOT_FOUND\n | typeof INVALID_COLUMN_WIDTH\n | typeof ROW_CLASS_ERROR\n | typeof CELL_CLASS_ERROR\n | typeof FORMAT_ERROR\n | typeof VIEW_MOUNT_ERROR\n | typeof VIEW_DISPATCH_ERROR\n | typeof TOOL_PANEL_MISSING_ATTR\n | typeof NO_TOOL_PANELS\n | typeof TOOL_PANEL_NOT_FOUND\n | typeof TOOL_PANEL_DUPLICATE\n | typeof HEADER_CONTENT_DUPLICATE\n | typeof TOOLBAR_CONTENT_DUPLICATE\n | typeof EDITOR_MOUNT_ERROR\n | typeof PRINT_IN_PROGRESS\n | typeof PRINT_NO_GRID\n | typeof PRINT_FAILED\n | typeof PRINT_DUPLICATE_ID\n | typeof CLIPBOARD_FAILED\n | typeof MISSING_BREAKPOINT\n | typeof TRANSACTION_IN_PROGRESS\n | typeof NO_TRANSACTION\n | typeof COLUMN_GROUP_NO_ID\n | typeof COLUMN_GROUPS_CONFLICT\n | typeof STYLE_EXTRACT_FAILED\n | typeof STYLE_NOT_FOUND\n | typeof INVALID_ATTRIBUTE_JSON\n | typeof DATASOURCE_FETCH_ERROR\n | typeof DATASOURCE_CHILD_FETCH_ERROR\n | typeof DATASOURCE_NO_CHILD_HANDLER\n | typeof DATASOURCE_THROTTLED;\n\n// #endregion\n\n// #region Docs URL\n\nconst DOCS_BASE = 'https://toolboxjs.com/grid/errors';\n\n/** Build a direct link to the troubleshooting section for a code. */\nfunction docsUrl(code: DiagnosticCode): string {\n return `${DOCS_BASE}#${code.toLowerCase()}`;\n}\n\n// #endregion\n\n// #region Formatting\n\n/**\n * Format a diagnostic message with prefix, code, and docs link.\n *\n * Output format:\n * ```\n * [tbw-grid#my-id] TBW001: Your message here.\n *\n * → More info: https://toolboxjs.com/grid/errors#tbw001\n * ```\n */\nexport function formatDiagnostic(code: DiagnosticCode, message: string, gridId?: string, pluginName?: string): string {\n const prefix = gridPrefix(gridId, pluginName);\n return `${prefix} ${code}: ${message}\\n\\n → More info: ${docsUrl(code)}`;\n}\n\n// #endregion\n\n// #region Public API\n\n/**\n * Throw an error with a diagnostic code and docs link.\n * Use for configuration errors and API misuse that should halt execution.\n */\nexport function throwDiagnostic(code: DiagnosticCode, message: string, gridId?: string, pluginName?: string): never {\n throw new Error(formatDiagnostic(code, message, gridId, pluginName));\n}\n\n/**\n * Log a warning with a diagnostic code and docs link.\n * Use for recoverable issues the developer should fix.\n */\nexport function warnDiagnostic(code: DiagnosticCode, message: string, gridId?: string, pluginName?: string): void {\n console.warn(formatDiagnostic(code, message, gridId, pluginName));\n}\n\n/**\n * Log a debug message with a diagnostic code and docs link.\n * Use for optional/soft dependency notifications — visible only when\n * the browser DevTools \"Verbose\" log level is enabled.\n */\nexport function debugDiagnostic(code: DiagnosticCode, message: string, gridId?: string, pluginName?: string): void {\n console.debug(formatDiagnostic(code, message, gridId, pluginName));\n}\n\n/**\n * Log an error with a diagnostic code and docs link.\n * Use for non-throwing errors (e.g., failed async operations).\n */\nexport function errorDiagnostic(code: DiagnosticCode, message: string, gridId?: string, pluginName?: string): void {\n console.error(formatDiagnostic(code, message, gridId, pluginName));\n}\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 { HeaderContentDefinition, ToolPanelDefinition } from '../../plugins/shell/types';\nimport { DEFAULT_GRID_ICONS } from '../defaults';\nimport {\n type DiagnosticCode,\n PLUGIN_ALIAS_CONFIG_CONFLICT,\n formatDiagnostic,\n gridPrefix,\n} from '../internal/diagnostics';\nimport { setSanitizedHTML } from '../internal/sanitize';\nimport type {\n ColumnConfig,\n ColumnState,\n GridConfig,\n GridIcons,\n GridPlugin,\n IconValue,\n PluginNameMap,\n Translate,\n} from '../types';\n\n// Re-export shared plugin types for convenience\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 GridPlugin>(PluginClass: new (...args: any[]) => T): T | undefined;\n getPluginByName<K extends string>(\n name: K,\n ): (K extends keyof PluginNameMap ? PluginNameMap[K] : 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 // Focus Management\n /** Register an external DOM element as a logical focus container. @internal Plugin API */\n registerExternalFocusContainer?(el: Element): void;\n /** Unregister a previously registered external focus container. @internal Plugin API */\n unregisterExternalFocusContainer?(el: Element): void;\n /** Check whether focus is logically inside this grid (own DOM + external containers). @internal Plugin API */\n containsFocus?(node?: Node | null): boolean;\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 * @since 0.4.1\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. Silent if missing\n * (set {@link severity} to `'warn'` or `'info'` to opt into a message).\n * @default true\n */\n required?: boolean;\n\n /**\n * Explicit severity when this dependency is missing. Overrides the default\n * derived from {@link required}.\n * - `'error'`: throw (halts grid setup). Default when `required` is not `false`.\n * - `'warn'`: log a console warning and continue (development only).\n * - `'info'`: log a verbose console.debug message and continue (development only).\n *\n * When omitted, a missing hard dependency throws and a missing soft\n * dependency is silent — preserving backward-compatible behavior.\n * @since 2.16.0\n */\n severity?: 'error' | 'warn' | 'info';\n\n /**\n * Optional predicate that gates whether this dependency applies, evaluated\n * against the depending plugin's own resolved configuration (defaults merged\n * with user config). Return `false` to skip the dependency entirely.\n *\n * Lets a plugin require/recommend another plugin only under certain config\n * — e.g. a plugin that needs a tool-panel host only when its tool panel is\n * enabled. When omitted, the dependency always applies.\n * @example when: (cfg: PivotConfig) => cfg.showToolPanel === true\n * @since 2.16.0\n */\n when?: (pluginConfig: unknown) => 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 \"Both transform the entire row model in different ways\"\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 * @since 1.8.0\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 * @since 1.8.0\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 | 'processConfig'\n | 'processRows'\n | 'afterStructuralRender'\n | 'afterRender'\n | 'afterCellRender'\n | 'afterRowRender'\n | 'onCellClick'\n | 'onCellMouseDown'\n | 'onCellMouseMove'\n | 'onCellMouseUp'\n | 'onHeaderClick'\n | 'onKeyDown'\n | 'onRowClick'\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 * @since 1.1.0\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: 'tree', reason: 'Both transform the entire row model in different ways' },\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 * Whether this plugin's `processRows` hook injects or removes rows\n * (group headers, tree nodes, pivot aggregates, placeholders, etc.).\n *\n * When true, the core sorting module delegates to the render scheduler\n * instead of sorting `_rows` in-place, because `_rows` may contain\n * plugin-generated marker objects that would be corrupted by a direct sort.\n *\n * Plugins that only **filter** or **reorder** existing rows (e.g. Filtering,\n * MultiSort) should leave this unset or set it to `false`.\n */\n modifiesRowStructure?: boolean;\n}\n\n/** Convert camelCase icon key to kebab-case data-icon attribute value. */\nexport function toIconAttr(key: string): string {\n return key.replace(/([A-Z])/g, '-$1').toLowerCase();\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 * @since 0.1.1\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. Optional dependencies are\n * silent by default; set a {@link PluginDependency.severity} of `'warn'` or\n * `'info'` to opt into a development-only log.\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 * Alternative names for backward compatibility.\n * `getPluginByName()` matches against both `name` and `aliases`.\n * @internal\n */\n readonly aliases?: readonly 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 via `document.adoptedStyleSheets` */\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 * Effective configuration (defaults merged with user config), resolved\n * eagerly so it is available **before** {@link attach} runs. Dependency\n * validation happens before `attach()` merges {@link config}, so config\n * -conditional dependency `when` predicates evaluate against this getter.\n *\n * While attached, returns the already-merged {@link config}; otherwise\n * recomputes `defaultConfig` + `userConfig` fresh. `config` is intentionally\n * **not** trusted when detached because {@link detach} does not clear it — a\n * re-validated plugin would otherwise see stale config from a prior\n * attachment. The {@link attach}-managed abort controller is the\n * attached/detached signal.\n * @internal Plugin infrastructure (used by `validatePluginDependencies`).\n */\n get resolvedConfig(): TConfig {\n return (this.#abortController ? this.config : { ...this.defaultConfig, ...this.userConfig }) as TConfig;\n }\n\n /**\n * Merge user-supplied configuration from other plugin instances into this\n * plugin's `userConfig`. Used by `PluginManager.attachAll()`'s alias-collapse\n * pre-pass: when a consumer instantiates the same plugin more than once (or\n * under an alias name), only one instance is attached, and the other\n * instances' configs are folded in via this method.\n *\n * Merge rules (shallow):\n *\n * - Same key, **same value** → silent.\n * - Same key, **different values** (incl. function vs function) → throws\n * a diagnostic naming the conflicting key. Conflicting callbacks\n * (`canDrag`, `canDrop`, etc.) cannot be reconciled automatically.\n * - **Disjoint keys** → merged cleanly.\n *\n * Subclasses may override to implement custom deep-merge logic, but should\n * call `super.mergeConfigsFrom(others)` to inherit the conflict-detection.\n *\n * @internal Plugin infrastructure (used by `PluginManager.attachAll`).\n * @throws Error with diagnostic code `TBW025` when configs conflict.\n */\n mergeConfigsFrom(others: readonly BaseGridPlugin<TConfig>[]): void {\n if (others.length === 0) return;\n const merged = { ...this.userConfig } as Record<string, unknown>;\n const sources: Record<string, BaseGridPlugin<TConfig>> = {};\n for (const key of Object.keys(merged)) sources[key] = this;\n for (const other of others) {\n const otherConfig = other.userConfig as Record<string, unknown>;\n for (const [key, value] of Object.entries(otherConfig)) {\n if (value === undefined) continue;\n if (!(key in merged)) {\n merged[key] = value;\n sources[key] = other;\n continue;\n }\n if (merged[key] === value) continue;\n // Conflict\n const ownerA = sources[key]?.constructor.name ?? this.constructor.name;\n const ownerB = other.constructor.name;\n const code = formatDiagnostic(\n PLUGIN_ALIAS_CONFIG_CONFLICT,\n `Cannot merge plugin configs for \"${this.name}\": conflicting value for \"${key}\" supplied by both ${ownerA} and ${ownerB}. Pass the option on a single instance, or remove the duplicate.`,\n undefined,\n this.name,\n );\n throw new Error(code);\n }\n }\n // Mutate userConfig in place so re-attach picks it up via attach().\n // `userConfig` is declared `readonly` to prevent reassigning the binding,\n // but the object's own properties are intentionally mutable for this\n // alias-collapse pre-pass. A single cast to a record is enough — no\n // `as unknown as` double-cast needed.\n Object.assign(this.userConfig as Record<string, unknown>, merged);\n }\n\n /**\n * Replace this plugin's `userConfig` with a freshly-resolved sibling\n * instance's config. Used by the grid's per-grid feature-instance gate\n * (`#initializePlugins`): when a feature is re-resolved across a config\n * change, the **existing** instance is reused — so its accumulated state\n * survives — but it must adopt the latest `gridConfig.features.<name>`\n * value. Unlike {@link mergeConfigsFrom}, this is last-write-wins with no\n * conflict detection: the re-resolved instance is authoritative.\n *\n * `attach()` recomputes {@link config} from `userConfig`, so updating\n * `userConfig` here is enough for the reused instance to pick up the new\n * config on its next attach during the same `#initializePlugins` pass.\n *\n * @internal Plugin infrastructure (used by the grid feature gate).\n */\n refreshUserConfigFrom(other: BaseGridPlugin<TConfig>): void {\n const target = this.userConfig as Record<string, unknown>;\n // Snapshot the source BEFORE clearing `target`. A feature factory may\n // store the consumer's config object by reference (e.g. `new\n // ContextMenuPlugin(config)`), and a consumer that passes the *same* config\n // object across re-resolutions (common when `gridConfig` is a recomputed\n // Angular `computed()`) makes `this.userConfig === other.userConfig`.\n // Without this snapshot, the `delete` loop below would empty that shared\n // object first, then `Object.assign` would copy back nothing — silently\n // wiping the config and falling back to plugin defaults. (#contextMenu)\n const source = { ...(other.userConfig as Record<string, unknown>) };\n for (const key of Object.keys(target)) delete target[key];\n Object.assign(target, source);\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 * **Prefer {@link BaseGridPlugin.grid grid.getPluginByName()}** when you don't need the class import.\n *\n * @example\n * ```ts\n * // Preferred: by name\n * const selection = this.grid?.getPluginByName('selection');\n *\n * // Alternative: by class\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 * Emit an event to **both** the plugin Event Bus (for inter-plugin communication)\n * **and** the DOM (for external consumers via `addEventListener`).\n *\n * Use this when a state change is relevant to both other plugins and external\n * consumers. For example, `sort-change` needs to invalidate Selection (plugin bus)\n * and notify the host application (DOM event).\n *\n * @category Plugin Development\n * @param eventType - The event type to broadcast\n * @param detail - The event payload\n *\n * @example\n * ```typescript\n * // Notify both plugins and consumers of a sort change\n * this.broadcast('sort-change', { sortModel: [...this.sortModel] });\n * ```\n */\n protected broadcast<T>(eventType: string, detail: T): void {\n this.emitPluginEvent(eventType, detail);\n this.emit(eventType, detail);\n }\n\n /**\n * Run a yes/no plugin query and reduce the responses to a single boolean.\n *\n * `grid.query()` always returns an **array** of responses (empty when no\n * plugin handles the query), and `[]` is truthy in JavaScript — so testing\n * the raw result directly (`if (grid.query(...))`) is always `true` and\n * silently takes the \"some plugin said yes\" branch even when nothing\n * answered. Always route boolean queries through this helper.\n *\n * @category Plugin Development\n * @param type - The query type to dispatch\n * @param context - Optional query context passed to the handlers\n * @returns `true` only when at least one plugin answered truthily\n *\n * @example\n * ```typescript\n * if (this.queryBoolean('datasource:is-active')) {\n * // A ServerSide data source really is installed\n * }\n * ```\n * @since 3.4.0\n */\n protected queryBoolean(type: string, context: unknown = null): boolean {\n const responses = this.grid?.query?.(type, context);\n // Tolerate hosts/tests that hand back a bare value instead of the array.\n return Array.isArray(responses) ? responses.some(Boolean) : !!responses;\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 * Re-render visible rows without rebuilding the row model or recalculating geometry.\n * Use this when row data has been updated in-place (e.g., server-side block loads)\n * and only the visible viewport needs to re-render.\n */\n protected requestVirtualRefresh(): void {\n this.grid?.requestVirtualRefresh?.();\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?._hostElement;\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(): Required<GridIcons> {\n const userIcons = this.grid?.gridConfig?.icons ?? {};\n return { ...DEFAULT_GRID_ICONS, ...userIcons };\n }\n\n /**\n * Resolve a user-visible string from {@link GridConfig.locale}, falling back to\n * the English default supplied at the call site.\n *\n * The fallback is passed inline rather than read from a shipped default map so\n * that a plugin's strings live in the plugin's own chunk.\n *\n * @param key - Namespaced message id, e.g. `'filter.apply'`.\n * @param fallback - English text used when the key is not overridden.\n * @since 3.5.0\n *\n * @example\n * ```ts\n * applyBtn.textContent = this.t('filter.apply', 'Apply');\n * ```\n */\n protected t(key: string, fallback: string): string {\n return this.grid?.effectiveConfig?.locale?.[key] ?? fallback;\n }\n\n /**\n * A bound `t()` suitable for handing to standalone renderer\n * helpers and public renderer callbacks.\n * @since 3.5.0\n */\n protected get translate(): Translate {\n return (key, fallback) => this.t(key, fallback);\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 * Set an icon on an element using the CSS-first hybrid approach.\n *\n * Sets `data-icon` for CSS pseudo-element rendering and `data-expanded` for\n * expand/collapse toggle elements. Only injects DOM content when a JS override\n * is present (via `gridConfig.icons` or `pluginOverride`), which naturally\n * suppresses the CSS `::before` via the `:empty` selector.\n *\n * @param element - The element to set the icon on\n * @param iconKey - The icon key in GridIcons (e.g., 'expand', 'collapse')\n * @param pluginOverride - Optional plugin-level override (string or HTMLElement)\n */\n protected setIcon(element: HTMLElement, iconKey: keyof GridIcons, pluginOverride?: IconValue): void {\n // Set data-icon attribute for CSS targeting (always)\n element.dataset.icon = toIconAttr(iconKey);\n\n // Set data-expanded for toggle elements (tree, group, master-detail)\n if (iconKey === 'collapse') {\n element.dataset.expanded = '';\n } else if (iconKey === 'expand') {\n delete element.dataset.expanded;\n }\n\n // Resolve JS-only icon (returns undefined when CSS default should be used)\n const jsIcon = this.#resolveJsIcon(iconKey, pluginOverride);\n\n if (jsIcon !== undefined) {\n // JS override: inject content (suppresses CSS ::before via :empty)\n if (typeof jsIcon === 'string') {\n setSanitizedHTML(element, jsIcon);\n } else if (jsIcon instanceof HTMLElement) {\n element.replaceChildren(jsIcon.cloneNode(true));\n }\n } else {\n // CSS path: ensure element is empty so ::before renders\n element.innerHTML = '';\n }\n }\n\n /**\n * Resolve JS-only icon override. Returns undefined if CSS default should render.\n * Only returns a value when the user explicitly set gridConfig.icons[key] or\n * the plugin passed an override.\n */\n #resolveJsIcon(iconKey: keyof GridIcons, pluginOverride?: IconValue): IconValue | undefined {\n if (pluginOverride !== undefined) return pluginOverride;\n return this.grid?.gridConfig?.icons?.[iconKey];\n }\n\n /**\n * Create or replace a sort indicator on a header cell.\n *\n * Handles the full lifecycle: removes any existing indicator, creates a new\n * `<span part=\"sort-indicator\" class=\"sort-indicator\">` with the appropriate\n * icon, sets `aria-sort` / `data-sort` attributes, and inserts it before the\n * filter button or resize handle.\n *\n * Plugins decide **which** cells and **when** — this method handles **how**.\n *\n * @param cell - The header cell element\n * @param direction - `'asc'` | `'desc'` for sorted, `null` for unsorted idle state\n * @returns The created indicator element (useful for appending badges, etc.)\n */\n protected updateSortIndicator(cell: Element, direction: 'asc' | 'desc' | null): HTMLElement {\n // Remove any existing indicator (core uses part, plugins use class)\n cell.querySelector('[part~=\"sort-indicator\"], .sort-indicator')?.remove();\n\n const indicator = document.createElement('span');\n indicator.setAttribute('part', 'sort-indicator');\n indicator.className = 'sort-indicator';\n\n if (direction) {\n cell.setAttribute('aria-sort', direction === 'asc' ? 'ascending' : 'descending');\n cell.setAttribute('data-sort', direction);\n this.setIcon(indicator, direction === 'asc' ? 'sortAsc' : 'sortDesc');\n } else {\n cell.setAttribute('aria-sort', 'none');\n cell.removeAttribute('data-sort');\n this.setIcon(indicator, 'sortNone');\n }\n\n // Insert before filter button or resize handle to maintain DOM order\n const insertBefore = cell.querySelector('.tbw-filter-btn') ?? cell.querySelector('.resize-handle');\n if (insertBefore) {\n cell.insertBefore(indicator, insertBefore);\n } else {\n cell.appendChild(indicator);\n }\n\n return indicator;\n }\n\n /**\n * Log a warning with an optional diagnostic code.\n *\n * When a diagnostic code is provided, the message is formatted with the code\n * and a link to the troubleshooting docs.\n *\n * @example\n * ```ts\n * this.warn('Something went wrong'); // plain\n * this.warn(MISSING_BREAKPOINT, 'Set a breakpoint'); // with code + docs link\n * ```\n */\n protected warn(message: string): void;\n protected warn(code: DiagnosticCode, message: string): void;\n protected warn(codeOrMessage: DiagnosticCode | string, message?: string): void {\n if (message !== undefined) {\n // Called with (code, message)\n console.warn(formatDiagnostic(codeOrMessage as DiagnosticCode, message, this.gridElement.id, this.name));\n } else {\n // Called with (message) — plain warning, no diagnostic code\n console.warn(`${gridPrefix(this.gridElement.id, this.name)} ${codeOrMessage}`);\n }\n }\n\n /**\n * Throw an error with a diagnostic code and docs link.\n * Use for configuration errors and API misuse that should halt execution.\n */\n protected throwDiagnostic(code: DiagnosticCode, message: string): never {\n throw new Error(formatDiagnostic(code, message, this.gridElement.id, this.name));\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 * Contribute to the grid's effective configuration during each config merge.\n * Called by {@link ConfigManager.merge} after the base config is assembled,\n * before it becomes `effectiveConfig`. Mutate `config` in place to inject\n * plugin-derived settings (e.g. the shell plugin folds its light-DOM /\n * API-registered state into `config.shell`).\n *\n * This is the config-pipeline counterpart to {@link processColumns} /\n * {@link processRows}: it keeps the core config merge free of any plugin\n * knowledge — each plugin owns the shape it contributes.\n *\n * @param config - The mutable base config being assembled into effectiveConfig.\n *\n * @example\n * ```ts\n * processConfig(config: GridConfig): void {\n * config.shell = { ...config.shell, header: { title: this.#title } };\n * }\n * ```\n */\n processConfig?(config: GridConfig): void;\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-grid-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 synchronously immediately after the grid (re)builds its root DOM\n * structure, before the browser has a chance to paint.\n *\n * Unlike {@link afterRender} (which runs in the scheduler's STYLE phase and\n * may be deferred), this hook fires inside the same task as the structural\n * DOM build. It is the correct place for plugins that need to wrap, relocate,\n * or augment the freshly built grid DOM without a flash of unstyled/unwrapped\n * content — e.g. a shell that moves the grid content into a chrome wrapper.\n *\n * The grid emits this hook every time it performs a full structural rebuild\n * (initial connect and structural changes), but NOT on the scroll/data hot\n * path. Implementations MUST be idempotent: a full rebuild discards any DOM a\n * previous invocation produced, so re-create it, and no-op when the desired\n * structure is already present.\n *\n * @example\n * ```ts\n * afterStructuralRender(): void {\n * const root = this.grid?._renderRoot;\n * if (!root || root.querySelector('.my-wrapper > .tbw-grid-content')) return;\n * // build wrapper, move .tbw-grid-content inside it\n * }\n * ```\n */\n afterStructuralRender?(): void;\n\n /**\n * Called synchronously by the grid immediately before it tears down and\n * rebuilds its root DOM structure (a structural re-render). Plugins that\n * relocated existing light-DOM nodes into their own chrome (e.g. a shell that\n * moves toolbar buttons into the header bar) use this to move those nodes back\n * to their original container so they survive the rebuild. Generic\n * counterpart to {@link afterStructuralRender}. MUST be idempotent.\n */\n beforeStructuralRender?(): void;\n\n /**\n * Called by the grid after a config merge to ask whether the change requires a\n * full structural re-render (rather than a cheaper in-place update). A plugin\n * returns `true` when its rendered DOM signature no longer matches the new\n * effective config — e.g. a shell header appearing/disappearing or a tool\n * panel's count/position/mode changing. The grid performs a structural\n * re-render if ANY plugin returns `true`. MUST be side-effect free.\n */\n needsStructuralRerender?(): boolean;\n\n /**\n * Called by the grid to let a plugin (re-)parse its own light-DOM\n * contributions into plugin state ahead of a config merge. Generic seam that\n * keeps core free of plugin-specific light-DOM tag knowledge (the plugin reads\n * `this.grid._hostElement`). MUST be idempotent.\n */\n parseLightDom?(): void;\n\n /**\n * Called by the grid (from `refreshColumns` and the light-DOM observer) to\n * let a plugin re-parse its light-DOM contributions AND refresh any\n * already-rendered chrome in place when something changed — e.g. a shell\n * title or tool buttons that a framework projected asynchronously after the\n * initial render. Distinct from {@link parseLightDom}, which only updates\n * plugin state. Generic seam — core holds no plugin-specific knowledge. MUST\n * be idempotent and a no-op when the plugin's chrome is not yet rendered.\n */\n syncLightDom?(): 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 * 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 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 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 /**\n * Report vertical scroll boundary offsets for this plugin.\n * Plugins that overlay the top or bottom of the rows viewport (e.g. sticky\n * row clones) should return how much space they obscure on each edge, so\n * keyboard navigation can scroll a focused row clear of the overlay rather\n * than leaving it hidden underneath — WCAG 2.2 SC 2.4.11 Focus Not Obscured.\n *\n * Only report space that is painted *over* the scrollable rows. Content that\n * takes part in normal flow (pinned-row bars, the column header) already\n * shrinks the viewport and must NOT be reported here, or rows would scroll\n * twice as far as needed.\n *\n * @param focusedRowIndex - Index of the row being scrolled into view, so a plugin can report `skipScroll` when it already renders that row in a permanently visible overlay\n * @returns Object with top/bottom pixel offsets and optional skipScroll flag, or undefined if plugin obscures nothing\n *\n * @example\n * ```ts\n * getVerticalScrollOffsets(focusedRowIndex?: number): { top: number; bottom: number; skipScroll?: boolean } | undefined {\n * if (!this.overlayEl) return undefined;\n * // The row is already pinned in the overlay — it is visible where it is.\n * if (focusedRowIndex != null && this.stuckIndices.includes(focusedRowIndex)) {\n * return { top: 0, bottom: 0, skipScroll: true };\n * }\n * return { top: this.overlayEl.offsetHeight, bottom: 0 };\n * }\n * ```\n *\n * @since 3.6.0\n */\n getVerticalScrollOffsets?(\n focusedRowIndex?: number,\n ): { top: number; bottom: 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.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 lockPosition: true,\n utility: true, // Marks this as a utility column (excluded from selection, clipboard, etc.)\n meta: {\n expanderColumn: true,\n expanderPlugin: pluginName,\n },\n };\n}\n","/**\n * Row Grouping Core Logic\n *\n * Pure functions for building grouped row models and aggregations.\n */\n\n/* eslint-disable @typescript-eslint/no-explicit-any */\n\nimport type { DefaultExpandedValue, GroupDefinition, GroupingRowsConfig, GroupRowModelItem, RenderRow } from './types';\n\ninterface GroupNode {\n key: string; // composite key\n value: any;\n depth: number;\n rows: any[];\n children: Map<string, GroupNode>;\n parent?: GroupNode;\n}\n\ninterface BuildGroupingArgs {\n rows: any[];\n config: GroupingRowsConfig;\n expanded: Set<string>;\n /** Initial expanded state to apply (processed by the plugin) */\n initialExpanded?: Set<string>;\n /** Sort direction per group depth level. 1 = ascending, -1 = descending.\n * When omitted, groups at all levels sort ascending. */\n groupSortDirections?: Map<number, 1 | -1>;\n}\n\n/**\n * Build a flattened grouping projection (collapsed by default).\n * Returns empty array when groupOn not configured or all rows ungrouped.\n *\n * @param args - The grouping arguments\n * @returns Flattened array of render rows (groups + data rows)\n */\nexport function buildGroupedRowModel({\n rows,\n config,\n expanded,\n initialExpanded,\n groupSortDirections,\n}: BuildGroupingArgs): RenderRow[] {\n const groupOn = config.groupOn;\n if (typeof groupOn !== 'function') {\n return [];\n }\n\n const root: GroupNode = { key: '__root__', value: null, depth: -1, rows: [], children: new Map() };\n\n // Build tree structure — push each row into every ancestor along the path\n // so that each group's `rows` array contains ALL data rows in its subtree.\n // This is required for correct counts and aggregations on multi-level groups.\n rows.forEach((r) => {\n let path: any = groupOn(r);\n if (path == null || path === false) path = ['__ungrouped__'];\n else if (!Array.isArray(path)) path = [path];\n\n let parent = root;\n path.forEach((rawVal: any, depthIdx: number) => {\n const seg = rawVal == null ? '∅' : String(rawVal);\n const composite = parent.key === '__root__' ? seg : parent.key + '||' + seg;\n let node = parent.children.get(seg);\n if (!node) {\n node = { key: composite, value: rawVal, depth: depthIdx, rows: [], children: new Map(), parent };\n parent.children.set(seg, node);\n }\n node.rows.push(r);\n parent = node;\n });\n });\n\n // All ungrouped? treat as no grouping\n if (root.children.size === 1 && root.children.has('__ungrouped__')) {\n const only = root.children.get('__ungrouped__')!;\n if (only.rows.length === rows.length) return [];\n }\n\n // Pre-build row→index map for O(1) lookups (avoids O(n²) from rows.indexOf)\n const rowIndexMap = new Map<any, number>();\n for (let i = 0; i < rows.length; i++) {\n rowIndexMap.set(rows[i], i);\n }\n\n // Merge expanded sets - use initialExpanded on first render, then expanded takes over\n const effectiveExpanded = new Set([...expanded, ...(initialExpanded ?? [])]);\n\n // Sort sibling groups by their group value so that group header order is\n // deterministic and respects the active sort direction for each depth level.\n // Default: ascending. When a user sorts a grouped column, the corresponding\n // depth level's direction flips, keeping groups in predictable order.\n const sortedChildren = (node: GroupNode): GroupNode[] => {\n const children = [...node.children.values()];\n // Determine direction for this depth level (children are one level deeper than the node)\n const childDepth = node === root ? 0 : node.depth + 1;\n const dir = groupSortDirections?.get(childDepth) ?? 1;\n children.sort((a, b) => {\n const av = a.value;\n const bv = b.value;\n if (av == null && bv == null) return 0;\n if (av == null) return dir;\n if (bv == null) return -dir;\n return av > bv ? dir : av < bv ? -dir : 0;\n });\n return children;\n };\n\n // Flatten tree to array\n const flat: RenderRow[] = [];\n const visit = (node: GroupNode) => {\n if (node === root) {\n for (const c of sortedChildren(node)) visit(c);\n return;\n }\n\n const isExpanded = effectiveExpanded.has(node.key);\n flat.push({\n kind: 'group',\n key: node.key,\n value: node.value,\n depth: node.depth,\n rows: node.rows,\n expanded: isExpanded,\n });\n\n if (isExpanded) {\n if (node.children.size) {\n for (const c of sortedChildren(node)) visit(c);\n } else {\n node.rows.forEach((r) => flat.push({ kind: 'data', row: r, rowIndex: rowIndexMap.get(r) ?? -1 }));\n }\n }\n };\n visit(root);\n\n return flat;\n}\n\n/**\n * Discover which column field produces the group value at each depth level.\n *\n * Samples the first row's `groupOn` output to get the group path, then checks\n * which column fields produce matching values for that row. This mapping allows\n * the plugin to apply user-invoked column sort directions to the correct group\n * depth levels.\n *\n * @returns Map from depth index to column field name, or empty map if unmappable\n */\nexport function resolveGroupFields(\n rows: any[],\n groupOn: (row: any) => any[] | any | null | false,\n columnFields: string[],\n): Map<number, string> {\n const depthToField = new Map<number, string>();\n if (rows.length === 0) return depthToField;\n\n const sampleRow = rows[0];\n let path: any = groupOn(sampleRow);\n if (path == null || path === false) return depthToField;\n if (!Array.isArray(path)) path = [path];\n\n for (let depth = 0; depth < path.length; depth++) {\n const groupValue = path[depth];\n for (const field of columnFields) {\n if (sampleRow[field] === groupValue) {\n depthToField.set(depth, field);\n break;\n }\n }\n }\n return depthToField;\n}\n\n/**\n * Toggle expansion state for a group key.\n *\n * @param expandedKeys - Current set of expanded keys\n * @param key - The group key to toggle\n * @returns New set with toggled state\n */\nexport function toggleGroupExpansion(expandedKeys: Set<string>, key: string): Set<string> {\n const newSet = new Set(expandedKeys);\n if (newSet.has(key)) {\n newSet.delete(key);\n } else {\n newSet.add(key);\n }\n return newSet;\n}\n\n/**\n * Expand all groups.\n *\n * @param rows - The flattened render rows\n * @returns Set of all group keys\n */\nexport function expandAllGroups(rows: RenderRow[]): Set<string> {\n const keys = new Set<string>();\n for (const row of rows) {\n if (row.kind === 'group') {\n keys.add(row.key);\n }\n }\n return keys;\n}\n\n/**\n * Collapse all groups.\n *\n * @returns Empty set\n */\nexport function collapseAllGroups(): Set<string> {\n return new Set();\n}\n\n/**\n * Resolve a defaultExpanded value to a set of keys to expand.\n * This needs to be called AFTER building the group model to get all keys.\n *\n * Note: `plugins/pivot/pivot-engine.ts` has a same-named function with the\n * opposite `undefined` default (expand-all). See the comment there.\n *\n * @param value - The defaultExpanded config value\n * @param allGroupKeys - All group keys from the model\n * @returns Set of keys to expand initially\n */\nexport function resolveDefaultExpanded(value: DefaultExpandedValue, allGroupKeys: string[]): Set<string> {\n if (value === true) {\n // Expand all groups\n return new Set(allGroupKeys);\n }\n if (value === false || value == null) {\n // Collapse all groups\n return new Set();\n }\n if (typeof value === 'number') {\n // Expand group at this index\n const key = allGroupKeys[value];\n return key ? new Set([key]) : new Set();\n }\n if (typeof value === 'string') {\n // Expand group with this key\n return new Set([value]);\n }\n if (Array.isArray(value)) {\n // Expand groups with these keys\n return new Set(value);\n }\n return new Set();\n}\n\n/**\n * Get all group keys from a flattened model.\n *\n * @param rows - The flattened render rows\n * @returns Array of group keys\n */\nexport function getGroupKeys(rows: RenderRow[]): string[] {\n return rows.filter((r): r is GroupRowModelItem => r.kind === 'group').map((r) => r.key);\n}\n\n/**\n * Count total rows in a group (including nested groups).\n *\n * @param groupRow - The group row\n * @returns Total row count\n */\nexport function getGroupRowCount(groupRow: RenderRow): number {\n if (groupRow.kind !== 'group') return 0;\n return groupRow.rows.length;\n}\n\n// #region Pre-Defined Group Model\n\ninterface PreDefinedGroupModelArgs {\n groups: GroupDefinition[];\n expanded: Set<string>;\n groupRows: Map<string, unknown[]>;\n loadingGroups: Set<string>;\n parentPath?: string[];\n}\n\n/**\n * Build a flattened render model from pre-defined group definitions.\n *\n * Unlike `buildGroupedRowModel`, this does not analyze row data — groups\n * are provided externally (e.g. from a server). Row data for each group\n * is populated lazily via the `groupRows` map.\n *\n * @param args - Pre-defined grouping arguments\n * @returns Flattened array of render rows (groups + data rows)\n */\nexport function buildPreDefinedGroupModel({\n groups,\n expanded,\n groupRows,\n loadingGroups,\n parentPath = [],\n}: PreDefinedGroupModelArgs): RenderRow[] {\n const flat: RenderRow[] = [];\n const depth = parentPath.length;\n\n for (const group of groups) {\n const currentPath = [...parentPath, group.key];\n const isExpanded = expanded.has(group.key);\n const rows = groupRows.get(group.key) ?? [];\n const isLoading = loadingGroups.has(group.key);\n\n flat.push({\n kind: 'group',\n key: group.key,\n value: group.value,\n depth,\n rows,\n expanded: isExpanded,\n });\n\n if (isExpanded) {\n // Nested child groups take priority over leaf rows\n if (group.children?.length) {\n const childRows = buildPreDefinedGroupModel({\n groups: group.children,\n expanded,\n groupRows,\n loadingGroups,\n parentPath: currentPath,\n });\n flat.push(...childRows);\n } else if (isLoading) {\n // Loading placeholder — rendered by the plugin as a loading indicator\n flat.push({ kind: 'data', row: { __loading: true, __groupKey: group.key }, rowIndex: -1 });\n } else {\n // Leaf rows from the groupRows map\n rows.forEach((row, idx) => {\n flat.push({ kind: 'data', row, rowIndex: idx });\n });\n }\n }\n }\n\n return flat;\n}\n\n/**\n * Compute the group path (array of ancestor keys) for a given group key\n * within a pre-defined group structure.\n *\n * @param groups - The group definitions to search\n * @param targetKey - The key to find\n * @param parentPath - Accumulated path (used for recursion)\n * @returns Array of group keys from root to target, or empty array if not found\n */\nexport function getGroupPath(groups: GroupDefinition[], targetKey: string, parentPath: string[] = []): string[] {\n for (const group of groups) {\n const currentPath = [...parentPath, group.key];\n if (group.key === targetKey) {\n return currentPath;\n }\n if (group.children?.length) {\n const found = getGroupPath(group.children, targetKey, currentPath);\n if (found.length > 0) return found;\n }\n }\n return [];\n}\n// #endregion\n","@layer tbw-plugins {\n /* Group row layout - uses grid template from parent */\n .group-row {\n display: grid;\n grid-template-columns: var(--tbw-column-template);\n background: var(--tbw-grouping-rows-bg, var(--tbw-color-panel-bg));\n font-weight: 500;\n border-bottom: var(--tbw-row-divider);\n min-height: var(--tbw-row-height);\n }\n .group-row .cell {\n display: flex;\n align-items: center;\n padding: var(--tbw-cell-padding, 0.125rem 0.5rem);\n }\n @media (hover: hover) {\n .group-row:hover {\n background: var(--tbw-grouping-rows-bg-hover, var(--tbw-color-row-hover));\n }\n }\n .group-toggle {\n cursor: pointer;\n user-select: none;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: var(--tbw-toggle-size, 1.25rem);\n height: var(--tbw-toggle-size, 1.25rem);\n margin-right: 0.25rem;\n background: none;\n border: 0;\n font: inherit;\n }\n .group-toggle:hover {\n background: var(--tbw-grouping-rows-toggle-hover, var(--tbw-color-row-hover));\n border-radius: var(--tbw-border-radius, 0.125rem);\n }\n\n /* SC 2.5.8: grow the target, not the box, so the group row keeps its height. */\n .group-toggle {\n position: relative;\n\n &::after {\n content: '';\n position: absolute;\n inset-block-start: 50%;\n inset-inline-start: 50%;\n translate: -50% -50%;\n inline-size: var(--tbw-touch-target-min, 24px);\n block-size: var(--tbw-touch-target-min, 24px);\n }\n }\n\n @media (pointer: coarse) {\n .group-toggle {\n min-width: var(--tbw-touch-target-min, 24px);\n min-height: var(--tbw-touch-target-min, 24px);\n }\n }\n .group-label {\n display: inline-flex;\n align-items: center;\n gap: var(--tbw-panel-gap, var(--tbw-spacing-md, 0.5rem));\n }\n .group-count {\n color: var(--tbw-grouping-rows-count-color, var(--tbw-color-fg-muted));\n font-size: var(--tbw-font-size-xs, 0.85em);\n font-weight: normal;\n }\n\n /* Aggregates container (for full-width mode) */\n .group-aggregates {\n display: inline-flex;\n align-items: center;\n gap: var(--tbw-spacing-lg, 1rem);\n margin-left: var(--tbw-spacing-lg, 1rem);\n font-weight: normal;\n font-size: var(--tbw-font-size-sm, 0.875em);\n color: var(--tbw-grouping-rows-aggregate-color, var(--tbw-color-fg-muted));\n }\n\n .group-aggregate {\n white-space: nowrap;\n }\n\n /* Depth-based indentation using CSS custom property */\n .group-row {\n padding-left: calc(var(--tbw-group-depth, 0) * var(--tbw-group-indent-width, 1.25em));\n }\n\n /* Slide animation for child rows appearing */\n .data-grid-row.tbw-group-slide-in {\n animation: tbw-group-slide-in var(--tbw-animation-duration, 200ms) var(--tbw-animation-easing, ease-out) forwards;\n }\n\n @keyframes tbw-group-slide-in {\n from {\n opacity: 0;\n transform: translateX(-8px);\n }\n to {\n opacity: 1;\n transform: translateX(0);\n }\n }\n\n /* Fade animation for child rows appearing */\n .data-grid-row.tbw-group-fade-in {\n animation: tbw-group-fade-in var(--tbw-animation-duration, 200ms) var(--tbw-animation-easing, ease-out) forwards;\n }\n\n @keyframes tbw-group-fade-in {\n from {\n opacity: 0;\n }\n to {\n opacity: 1;\n }\n }\n} /* End @layer tbw-plugins */\n","/**\n * Row Grouping Plugin (Class-based)\n *\n * Enables hierarchical row grouping with expand/collapse and aggregations.\n */\n\nimport { GridClasses } from '../../core/constants';\nimport { aggregatorRegistry } from '../../core/internal/aggregators';\nimport { announce, getA11yMessage } from '../../core/internal/aria';\nimport { setRowLoadingState } from '../../core/internal/loading';\nimport { setSanitizedHTML } from '../../core/internal/sanitize';\nimport {\n BaseGridPlugin,\n CellClickEvent,\n HeaderClickEvent,\n type GridElement,\n type PluginManifest,\n type PluginQuery,\n} from '../../core/plugin/base-plugin';\nimport { isExpanderColumn } from '../../core/plugin/expander-column';\nimport type { RowElementInternal } from '../../core/types';\nimport type {\n DataSourceChildrenDetail,\n DataSourceDataDetail,\n FetchChildrenQuery,\n ViewportMappingQuery,\n ViewportMappingResponse,\n} from '../server-side/datasource-types';\nimport {\n buildGroupedRowModel,\n buildPreDefinedGroupModel,\n collapseAllGroups,\n expandAllGroups,\n getGroupKeys,\n getGroupPath,\n getGroupRowCount,\n resolveDefaultExpanded,\n resolveGroupFields,\n toggleGroupExpansion,\n} from './grouping-rows';\nimport styles from './grouping-rows.css?inline';\nimport type {\n DefaultExpandedValue,\n ExpandCollapseAnimation,\n GroupCollapseDetail,\n GroupDefinition,\n GroupExpandDetail,\n GroupingRowsConfig,\n GroupRowModelItem,\n GroupToggleDetail,\n RenderRow,\n} from './types';\n\n/** Attribute write that skips the DOM mutation when the value is unchanged (per-frame hot path). */\nfunction setAttrIfChanged(el: Element, name: string, value: number): void {\n const next = String(value);\n if (el.getAttribute(name) !== next) el.setAttribute(name, next);\n}\n\n/**\n * Group state information returned by getGroupState()\n * @since 0.1.1\n */\nexport interface GroupState {\n /** Whether grouping is currently active */\n isActive: boolean;\n /** Number of expanded groups */\n expandedCount: number;\n /** Total number of groups */\n totalGroups: number;\n /** Array of expanded group keys */\n expandedKeys: string[];\n}\n\n/**\n * Row Grouping Plugin for tbw-grid\n *\n * Organizes rows into collapsible hierarchical groups. Perfect for organizing data\n * by category, department, status, or any other dimension—or even multiple dimensions\n * for nested grouping. Includes aggregation support for summarizing group data.\n *\n * ## Installation\n *\n * ```ts\n * import { GroupingRowsPlugin } from '@toolbox-web/grid/plugins/grouping-rows';\n * ```\n *\n * ## CSS Custom Properties\n *\n * | Property | Default | Description |\n * |----------|---------|-------------|\n * | `--tbw-group-indent-width` | `1.25em` | Indentation per group level |\n * | `--tbw-grouping-rows-bg` | `var(--tbw-color-panel-bg)` | Group row background |\n * | `--tbw-grouping-rows-count-color` | `var(--tbw-color-fg-muted)` | Count badge color |\n * | `--tbw-animation-duration` | `200ms` | Expand/collapse animation |\n *\n * @example Single-Level Grouping by Department\n * ```ts\n * import { queryGrid } from '@toolbox-web/grid';\n * import { GroupingRowsPlugin } from '@toolbox-web/grid/plugins/grouping-rows';\n *\n * const grid = queryGrid('tbw-grid');\n * grid.gridConfig = {\n * columns: [\n * { field: 'name', header: 'Employee' },\n * { field: 'department', header: 'Department' },\n * { field: 'salary', header: 'Salary', type: 'currency' },\n * ],\n * plugins: [\n * new GroupingRowsPlugin({\n * groupOn: (row) => [row.department],\n * showRowCount: true,\n * defaultExpanded: false,\n * }),\n * ],\n * };\n * ```\n *\n * @example Multi-Level Grouping\n * ```ts\n * new GroupingRowsPlugin({\n * groupOn: (row) => [row.region, row.department, row.team],\n * indentWidth: 24,\n * animation: 'slide',\n * })\n * ```\n *\n * @see {@link GroupingRowsConfig} for all configuration options\n * @see {@link GroupState} for the group state structure\n *\n * @internal Extends BaseGridPlugin\n * @since 0.1.1\n */\nexport class GroupingRowsPlugin extends BaseGridPlugin<GroupingRowsConfig> {\n /**\n * Plugin manifest - declares configuration validation rules and events.\n * @internal\n */\n static override readonly manifest: PluginManifest<GroupingRowsConfig> = {\n modifiesRowStructure: true,\n hookPriority: {\n processRows: 10, // Run after ServerSide (-10) so we receive managedNodes[]\n // Run before MultiSort so we can intercept header clicks on grouped columns\n onHeaderClick: -1,\n },\n incompatibleWith: [\n {\n name: 'tree',\n reason:\n 'Both plugins transform the entire row model. TreePlugin flattens nested hierarchies while ' +\n 'GroupingRowsPlugin groups flat rows with synthetic headers. Use one approach per grid.',\n },\n {\n name: 'pivot',\n reason:\n 'PivotPlugin creates its own aggregated row and column structure. ' +\n 'Row grouping cannot be applied on top of pivot-generated rows.',\n },\n ],\n events: [\n {\n type: 'group-toggle',\n description: 'Emitted when groups are expanded/collapsed. Broadcast to both DOM consumers and plugin bus.',\n },\n {\n type: 'group-expand',\n description: 'Emitted when a pre-defined group is expanded.',\n },\n {\n type: 'group-collapse',\n description: 'Emitted when a pre-defined group is collapsed.',\n },\n ],\n queries: [\n {\n type: 'canMoveRow',\n description: 'Returns false for group header rows (cannot be reordered)',\n },\n {\n type: 'grouping:get-grouped-fields',\n description: 'Returns the column field names that match group depth levels (string[])',\n },\n {\n type: 'datasource:viewport-mapping',\n description: 'Translates flat viewport row indices to top-level group indices for ServerSide pagination.',\n },\n ],\n configRules: [\n {\n id: 'groupingRows/accordion-defaultExpanded',\n severity: 'warn',\n message:\n `\"accordion: true\" and \"defaultExpanded\" (non-false) are used together.\\n` +\n ` → In accordion mode, only one group can be open at a time.\\n` +\n ` → Using defaultExpanded with multiple groups will collapse to one on first toggle.\\n` +\n ` → Consider using \"defaultExpanded: false\" or a single group key/index with accordion mode.`,\n check: (config) =>\n config.accordion === true &&\n config.defaultExpanded !== false &&\n config.defaultExpanded !== undefined &&\n // Allow single group expansion with accordion\n !(typeof config.defaultExpanded === 'number') &&\n !(typeof config.defaultExpanded === 'string') &&\n // Warn if true or array with multiple items\n (config.defaultExpanded === true ||\n (Array.isArray(config.defaultExpanded) && config.defaultExpanded.length > 1)),\n },\n ],\n };\n\n /**\n * Optional dependency on MultiSort for coordinated sort management.\n * When MultiSort is loaded, GroupingRows queries its sort model to determine\n * group header ordering. Without it, falls back to core sort state.\n */\n static override readonly dependencies = [\n { name: 'multiSort', required: false, reason: 'Queries sort model for coordinated group sorting' },\n { name: 'serverSide', required: false, reason: 'Consumes datasource events for lazy-loaded grouped data' },\n ];\n\n /** @internal */\n readonly name = 'groupingRows';\n /** @internal */\n override readonly styles = styles;\n\n /** @internal */\n protected override get defaultConfig(): Partial<GroupingRowsConfig> {\n return {\n defaultExpanded: false,\n showRowCount: true,\n indentWidth: 20,\n aggregators: {},\n animation: 'slide',\n accordion: false,\n };\n }\n\n // #region Internal State\n private expandedKeys: Set<string> = new Set();\n private flattenedRows: RenderRow[] = [];\n private isActive = false;\n private previousVisibleKeys = new Set<string>();\n private keysToAnimate = new Set<string>();\n /** Track if initial defaultExpanded has been applied */\n private hasAppliedDefaultExpanded = false;\n /**\n * Expansion override to apply on the next `processRows` rebuild instead of\n * — or in addition to — `config.defaultExpanded`. Set by `setGroupOn(fn, expanded)`\n * and by `expandAll()` / `collapseAll()` when the grouping config has just\n * changed (so `flattenedRows` is stale). Cleared once consumed by a rebuild.\n * See issue #335.\n */\n private pendingExpansion: DefaultExpandedValue | undefined;\n /**\n * Set by `setGroupOn` to mark the cached `flattenedRows` as stale until the\n * next `processRows` runs. While true, `expandAll`/`collapseAll` defer via\n * `pendingExpansion` instead of reading the stale snapshot. See issue #335.\n */\n private groupConfigDirty = false;\n /** Pre-defined group definitions (from config, setGroups(), or datasource:data) */\n private preDefinedGroups: GroupDefinition[] = [];\n /** Row data keyed by group key (from setGroupRows() or datasource:children) */\n private groupRowsMap = new Map<string, unknown[]>();\n /** Groups currently in a loading state */\n private loadingGroups = new Set<string>();\n /** Column fields that produce group values (depth 0, 1, ...). Cached for\n * the `grouping:get-grouped-fields` query so MultiSort can filter them out. */\n private groupedFields: string[] = [];\n /** User-specified sort directions per group depth level. Toggled via\n * header clicks on grouped columns. */\n private userGroupSortDirections = new Map<number, 1 | -1>();\n /**\n * Per-flatten-index ARIA position metadata for `aria-level` /\n * `aria-setsize` / `aria-posinset` (WAI-ARIA Treegrid pattern). Computed\n * once per `flattenedRows` rebuild by {@link computeFlatMeta}; consumed\n * by {@link afterRender} on every visible row.\n */\n private flatMeta: Array<{ level: number; setSize: number; posInSet: number }> = [];\n // #endregion\n\n // #region Animation\n\n /**\n * Get expand/collapse animation style from plugin config.\n * Uses base class isAnimationEnabled to respect grid-level settings.\n */\n private get animationStyle(): ExpandCollapseAnimation {\n if (!this.isAnimationEnabled) return false;\n return this.config.animation ?? 'slide';\n }\n\n // #endregion\n\n // #region Lifecycle\n\n /** @internal */\n override detach(): void {\n // Restore default `role=\"grid\"` on the rows-body so the grid stays\n // ARIA-valid after the plugin is removed (template default lives in\n // `core/internal/dom-builder.ts`). See WAI-ARIA Treegrid pattern.\n const rowsBody = this.gridElement?.querySelector('.rows-body');\n rowsBody?.setAttribute('role', 'grid');\n\n this.expandedKeys.clear();\n this.flattenedRows = [];\n this.flatMeta = [];\n this.isActive = false;\n this.previousVisibleKeys.clear();\n this.keysToAnimate.clear();\n this.hasAppliedDefaultExpanded = false;\n this.pendingExpansion = undefined;\n this.groupConfigDirty = false;\n this.preDefinedGroups = [];\n this.groupRowsMap.clear();\n this.loadingGroups.clear();\n this.groupedFields = [];\n this.userGroupSortDirections.clear();\n }\n\n /**\n * Provide row height for group header rows.\n *\n * If `groupRowHeight` is configured, returns that value for group rows.\n * This allows the variable row height system to use known heights for\n * group headers without needing to measure them from the DOM.\n *\n * @param row - The row object (may be a group row)\n * @param _index - Index in the processed rows array (unused)\n * @returns Height in pixels for group rows, undefined for data rows\n *\n * @internal Plugin hook for variable row height support\n */\n override getRowHeight(row: unknown, _index: number): number | undefined {\n // Only provide height if groupRowHeight is configured\n if (this.config.groupRowHeight == null) return undefined;\n\n // Check if this is a group row\n if ((row as { __isGroupRow?: boolean }).__isGroupRow === true) {\n return this.config.groupRowHeight;\n }\n\n return undefined;\n }\n\n /**\n * Handle plugin queries.\n * @internal\n */\n override handleQuery(query: PluginQuery): unknown {\n if (query.type === 'canMoveRow') {\n // Group header rows cannot be reordered\n const row = query.context as { __isGroupRow?: boolean } | null | undefined;\n if (row?.__isGroupRow === true) {\n return false;\n }\n }\n if (query.type === 'grouping:get-grouped-fields') {\n return [...this.groupedFields];\n }\n if (query.type === 'datasource:viewport-mapping') {\n // Translate visible flat row indices → top-level group indices for ServerSide pagination\n const { viewportStart, viewportEnd } = query.context as ViewportMappingQuery;\n if (this.flattenedRows.length === 0) return undefined;\n // Only respond in pre-defined groups mode (when datasource data is being consumed)\n if (this.preDefinedGroups.length === 0 && !Array.isArray(this.config.groups)) return undefined;\n\n const activeGroups = this.getActiveGroups();\n const startNode = this.getTopLevelGroupIndex(viewportStart);\n const endNode = this.getTopLevelGroupIndex(viewportEnd) + 1; // exclusive\n const totalLoadedNodes = activeGroups.length;\n\n return { startNode, endNode, totalLoadedNodes } satisfies ViewportMappingResponse;\n }\n return undefined;\n }\n\n /**\n * Translate a flat row index to the top-level group index it belongs to.\n * Walks flattenedRows up to the given index and counts depth-0 groups encountered.\n */\n private getTopLevelGroupIndex(flatIndex: number): number {\n let groupIndex = -1;\n const clampedIndex = Math.min(flatIndex, this.flattenedRows.length - 1);\n for (let i = 0; i <= clampedIndex; i++) {\n const row = this.flattenedRows[i];\n if (row.kind === 'group' && row.depth === 0) {\n groupIndex++;\n }\n }\n return Math.max(0, groupIndex);\n }\n\n // #region Lifecycle\n\n /** @internal */\n override attach(grid: GridElement): void {\n super.attach(grid);\n\n // Listen for datasource:data from ServerSidePlugin — claim data as group definitions\n this.on('datasource:data', (detail: unknown) => {\n const d = detail as DataSourceDataDetail;\n if (!d.claimed) {\n d.claimed = true;\n // Interpret incoming rows as group definitions\n this.preDefinedGroups = d.rows as unknown as GroupDefinition[];\n this.groupRowsMap.clear();\n this.loadingGroups.clear();\n this.hasAppliedDefaultExpanded = false;\n this.requestRender();\n }\n });\n\n // Listen for datasource:children — consume child rows from ServerSide\n this.on('datasource:children', (detail: unknown) => {\n const d = detail as DataSourceChildrenDetail;\n if (d.context?.source !== 'grouping-rows') return;\n d.claimed = true;\n\n // Store the rows for the group and clear loading state\n const groupKey = d.context.groupKey as string;\n if (groupKey) {\n this.groupRowsMap.set(groupKey, d.rows);\n this.loadingGroups.delete(groupKey);\n this.requestRender();\n }\n });\n }\n\n /**\n * Intercept header clicks on grouped columns to toggle group sort direction.\n * Returns `true` for grouped columns to prevent MultiSort from handling them.\n * @internal\n */\n override onHeaderClick(event: HeaderClickEvent): boolean | void {\n const fieldIndex = this.groupedFields.indexOf(event.field);\n if (fieldIndex === -1) return; // Not a grouped column — let other plugins handle it\n\n if (!event.column.sortable) return;\n\n // The fieldIndex in groupedFields corresponds to the group depth level\n // (resolveGroupFields populates them in depth order: 0, 1, 2, ...)\n const targetDepth = fieldIndex;\n\n // Toggle direction: asc → desc → asc\n const current = this.userGroupSortDirections.get(targetDepth) ?? 1;\n this.userGroupSortDirections.set(targetDepth, current === 1 ? -1 : 1);\n\n this.requestRender();\n return true; // Prevent MultiSort from handling this column\n }\n // #endregion\n\n // #region Sort State Resolution\n\n /**\n * Build a sort-direction map for each group depth level by cross-referencing\n * the active sort state with the column fields that produce group values.\n *\n * Supports three direction sources (in priority order):\n * 1. User-set directions via header clicks on grouped columns\n * 2. MultiSort plugin model (for backwards compatibility / state restore)\n * 3. Core single-column sort state\n */\n private resolveGroupSortDirections(rows: readonly any[]): Map<number, 1 | -1> | undefined {\n const config = this.config;\n if (typeof config.groupOn !== 'function' || rows.length === 0) {\n this.groupedFields = [];\n return undefined;\n }\n\n // Discover depth → field mapping by sampling rows\n const columnFields = this.columns.map((c) => c.field);\n const depthToField = resolveGroupFields([...rows], config.groupOn, columnFields);\n\n // Cache grouped field names for the grouping:get-grouped-fields query\n this.groupedFields = [...depthToField.values()];\n\n if (depthToField.size === 0) return undefined;\n\n // Start with user-set directions (from header clicks on grouped columns)\n const directions = new Map<number, 1 | -1>(this.userGroupSortDirections);\n\n // Fill missing depths from MultiSort model or core sort state\n if (directions.size < depthToField.size) {\n const activeSorts = new Map<string, 1 | -1>();\n\n const multiSortResults = this.grid?.query?.('sort:get-model', null);\n if (Array.isArray(multiSortResults) && multiSortResults.length > 0) {\n const sortModel = multiSortResults[0] as Array<{ field: string; direction: 'asc' | 'desc' }>;\n if (Array.isArray(sortModel)) {\n for (const entry of sortModel) {\n activeSorts.set(entry.field, entry.direction === 'desc' ? -1 : 1);\n }\n }\n }\n\n if (activeSorts.size === 0) {\n const sortState = this.grid._sortState;\n if (sortState) {\n activeSorts.set(sortState.field, sortState.direction);\n }\n }\n\n for (const [depth, field] of depthToField) {\n if (!directions.has(depth)) {\n const dir = activeSorts.get(field);\n if (dir !== undefined) {\n directions.set(depth, dir);\n }\n }\n }\n }\n\n return directions.size > 0 ? directions : undefined;\n }\n\n // #endregion\n\n // #region Hooks\n\n /**\n * Auto-detect grouping configuration from grid config.\n * Called by plugin system to determine if plugin should activate.\n */\n static detect(rows: readonly any[], config: any): boolean {\n return (\n typeof config?.groupOn === 'function' ||\n typeof config?.enableRowGrouping === 'boolean' ||\n Array.isArray(config?.groups)\n );\n }\n\n /** @internal */\n override processRows(rows: readonly any[]): any[] {\n // Snapshot + clear deferred-expansion state at the top so any early-return\n // (no groupOn, empty rows, etc.) doesn't leave a stale `pendingExpansion`\n // behind to be (mis-)applied to the next unrelated rebuild.\n const pendingExpansion = this.pendingExpansion;\n this.pendingExpansion = undefined;\n this.groupConfigDirty = false;\n\n // Pre-defined groups path — use external group structure instead of groupOn analysis\n if (this.preDefinedGroups.length > 0 || Array.isArray(this.config.groups)) {\n if (this.preDefinedGroups.length > 0 || (Array.isArray(this.config.groups) && this.config.groups.length > 0)) {\n return this.processPreDefinedGroups();\n }\n this.isActive = false;\n this.flattenedRows = [];\n return [];\n }\n\n const config = this.config;\n\n // Check if grouping is configured\n if (typeof config.groupOn !== 'function') {\n this.isActive = false;\n this.flattenedRows = [];\n return [...rows];\n }\n\n // First build: get structure to know all group keys\n // (needed for index-based defaultExpanded)\n const groupSortDirections = this.resolveGroupSortDirections(rows);\n const initialBuild = buildGroupedRowModel({\n rows: [...rows],\n config: config,\n expanded: new Set(), // Empty to get all root groups\n groupSortDirections,\n });\n\n // If no grouping produced, return original rows\n if (initialBuild.length === 0) {\n this.isActive = false;\n this.flattenedRows = [];\n return [...rows];\n }\n\n // Determine which expansion config to resolve against the *fresh* group\n // keys. A pending value (from `setGroupOn(fn, expanded)` or a deferred\n // `expandAll`/`collapseAll` after `setGroupOn`) takes priority and is\n // applied unconditionally — including the empty case (collapseAll). Falls\n // back to the first-render `defaultExpanded` resolution.\n let expansionConfig: DefaultExpandedValue | undefined;\n const isPendingApply = pendingExpansion !== undefined;\n if (isPendingApply) {\n expansionConfig = pendingExpansion;\n } else if (!this.hasAppliedDefaultExpanded && this.expandedKeys.size === 0 && config.defaultExpanded !== false) {\n expansionConfig = config.defaultExpanded ?? false;\n }\n\n let initialExpanded: Set<string> | undefined;\n if (expansionConfig !== undefined) {\n const allKeys = getGroupKeys(initialBuild);\n initialExpanded = resolveDefaultExpanded(expansionConfig, allKeys);\n if (isPendingApply) {\n // Pending overrides any stale keys — apply unconditionally.\n this.expandedKeys = new Set(initialExpanded);\n this.hasAppliedDefaultExpanded = true;\n } else if (initialExpanded.size > 0) {\n this.expandedKeys = new Set(initialExpanded);\n this.hasAppliedDefaultExpanded = true;\n }\n }\n\n // Build with proper expanded state\n const grouped = buildGroupedRowModel({\n rows: [...rows],\n config: config,\n expanded: this.expandedKeys,\n initialExpanded,\n groupSortDirections,\n });\n\n this.isActive = true;\n this.flattenedRows = grouped;\n this.computeFlatMeta();\n\n // Notify subscribers when a deferred expansion has been applied so React/\n // host code can mirror the new state without a follow-up call. Use\n // `broadcast` so DOM listeners (`grid.addEventListener('group-toggle')`)\n // see the change too — matches the manifest contract for `group-toggle`\n // and `toggle()`'s emit pattern.\n if (isPendingApply) {\n this.broadcast<GroupToggleDetail>('group-toggle', { expandedKeys: [...this.expandedKeys] });\n }\n\n // Track which data rows are newly visible (for animation)\n this.keysToAnimate.clear();\n const currentVisibleKeys = new Set<string>();\n grouped.forEach((item, idx) => {\n if (item.kind === 'data') {\n const key = `data-${idx}`;\n currentVisibleKeys.add(key);\n if (!this.previousVisibleKeys.has(key)) {\n this.keysToAnimate.add(key);\n }\n }\n });\n this.previousVisibleKeys = currentVisibleKeys;\n\n // Return flattened rows for rendering\n // The grid will need to handle group rows specially\n return grouped.map((item) => {\n if (item.kind === 'group') {\n return {\n __isGroupRow: true,\n __groupKey: item.key,\n __groupValue: item.value,\n __groupDepth: item.depth,\n __groupRows: item.rows,\n __groupExpanded: item.expanded,\n __groupRowCount: getGroupRowCount(item),\n // Cache key for variable row height support - survives expand/collapse\n __rowCacheKey: `group:${item.key}`,\n };\n }\n return item.row;\n });\n }\n\n /** @internal */\n override onCellClick(event: CellClickEvent): boolean | void {\n const row = event.row as Record<string, unknown> | undefined;\n\n // Check if this is a group row toggle\n if (row?.__isGroupRow) {\n const target = event.originalEvent.target as HTMLElement;\n if (target?.closest(`.${GridClasses.GROUP_TOGGLE}`)) {\n this.toggle(row.__groupKey as string);\n return true; // Prevent default\n }\n }\n }\n\n /** @internal */\n override onKeyDown(event: KeyboardEvent): boolean | void {\n // SPACE toggles expansion on group rows\n if (event.key !== ' ') return;\n\n const focusRow = this.grid._focusRow;\n const row = this.rows[focusRow] as Record<string, unknown> | undefined;\n\n // Only handle SPACE on group rows\n if (!row?.__isGroupRow) return;\n\n event.preventDefault();\n this.toggle(row.__groupKey as string);\n\n // Restore focus styling after render completes via render pipeline\n this.requestRenderWithFocus();\n return true;\n }\n\n /**\n * Render a row. Returns true if we handled the row (group row or loading placeholder), false otherwise.\n * @internal\n */\n override renderRow(row: any, rowEl: HTMLElement, _rowIndex: number): boolean {\n // Handle loading placeholder rows for pre-defined groups\n // Uses the grid's built-in row loading API for consistent, customizable spinners\n if (row?.__loading === true && row?.__groupKey) {\n rowEl.className = 'data-grid-row';\n (rowEl as RowElementInternal).__isCustomRow = true;\n rowEl.innerHTML = '';\n const cell = document.createElement('div');\n cell.className = 'cell';\n cell.style.gridColumn = '1 / -1';\n cell.setAttribute('role', 'gridcell');\n cell.textContent = '\\u00A0'; // &nbsp; for row height\n rowEl.appendChild(cell);\n setRowLoadingState(rowEl, true);\n return true;\n }\n\n // Only handle group rows\n if (!row?.__isGroupRow) {\n return false;\n }\n\n const config = this.config;\n\n // If a custom renderer is provided, use it\n if (config.groupRowRenderer) {\n const toggleExpand = () => {\n this.toggle(row.__groupKey);\n };\n\n const result = config.groupRowRenderer({\n key: row.__groupKey,\n value: row.__groupValue,\n depth: row.__groupDepth,\n rows: row.__groupRows,\n expanded: row.__groupExpanded,\n toggleExpand,\n });\n\n if (result) {\n rowEl.className = 'data-grid-row group-row';\n (rowEl as RowElementInternal).__isCustomRow = true; // Mark for proper class reset on recycle\n rowEl.setAttribute('data-group-depth', String(row.__groupDepth));\n if (typeof result === 'string') {\n setSanitizedHTML(rowEl, result);\n } else {\n rowEl.replaceChildren(result);\n }\n return true;\n }\n }\n\n // Helper to toggle expansion\n const handleToggle = () => {\n this.toggle(row.__groupKey);\n };\n\n // Default group row rendering - keep data-grid-row class for focus/keyboard navigation\n rowEl.className = 'data-grid-row group-row';\n (rowEl as RowElementInternal).__isCustomRow = true; // Mark for proper class reset on recycle\n rowEl.setAttribute('data-group-depth', String(row.__groupDepth));\n rowEl.setAttribute('role', 'row');\n rowEl.setAttribute('aria-expanded', String(row.__groupExpanded));\n // Public theming hook — see also TreePlugin / MasterDetailPlugin.\n rowEl.classList.toggle('tbw-row-expanded', !!row.__groupExpanded);\n // WAI-ARIA Treegrid: group rows announce their hierarchical position.\n const groupMeta = this.flatMeta[_rowIndex];\n if (groupMeta) {\n rowEl.setAttribute('aria-level', String(groupMeta.level));\n rowEl.setAttribute('aria-setsize', String(groupMeta.setSize));\n rowEl.setAttribute('aria-posinset', String(groupMeta.posInSet));\n }\n // Use CSS variable for depth-based indentation\n rowEl.style.setProperty('--tbw-group-depth', String(row.__groupDepth || 0));\n if (config.indentWidth !== undefined) {\n rowEl.style.setProperty('--tbw-group-indent-width', `${config.indentWidth}px`);\n }\n // Clear any inline height from previous use (e.g., responsive card mode sets height: auto)\n // This ensures group rows use CSS-defined height, not stale inline styles from recycled elements\n rowEl.style.height = '';\n rowEl.innerHTML = '';\n\n const isFullWidth = config.fullWidth !== false; // default true\n\n if (isFullWidth) {\n this.renderFullWidthGroupRow(row, rowEl, handleToggle);\n } else {\n this.renderPerColumnGroupRow(row, rowEl, handleToggle);\n }\n\n return true;\n }\n\n /** @internal */\n override afterRender(): void {\n const body = this.gridElement?.querySelector('.rows');\n if (!body) return;\n\n // Hierarchy is now in play \\u2192 swap the rows-body role from `grid` to\n // `treegrid` per WAI-ARIA so per-row level/setsize/posinset are valid\n // in context. Idempotent on the hot path.\n const rowsBody = this.gridElement?.querySelector('.rows-body');\n if (rowsBody && rowsBody.getAttribute('role') !== 'treegrid') {\n rowsBody.setAttribute('role', 'treegrid');\n }\n\n // Apply ARIA position metadata to every visible DATA row (group rows are\n // populated by `renderRow` directly since they bypass the default cell\n // template that exposes `data-row`). Single pass shared with the\n // expand/collapse animation so the hot path queries each row once.\n const style = this.animationStyle;\n const animate = style !== false && this.keysToAnimate.size > 0;\n const animClass = style === 'fade' ? 'tbw-group-fade-in' : 'tbw-group-slide-in';\n\n for (const rowEl of body.querySelectorAll('.data-grid-row:not(.group-row)')) {\n const cell = rowEl.querySelector('.cell[data-row]');\n const idx = cell ? parseInt(cell.getAttribute('data-row') ?? '-1', 10) : -1;\n const meta = this.flatMeta[idx];\n if (meta) {\n // Pooled row elements keep their position across most frames — skip\n // the attribute mutation unless the value actually changed.\n setAttrIfChanged(rowEl, 'aria-level', meta.level);\n setAttrIfChanged(rowEl, 'aria-setsize', meta.setSize);\n setAttrIfChanged(rowEl, 'aria-posinset', meta.posInSet);\n }\n\n if (animate && this.flattenedRows[idx]?.kind === 'data' && this.keysToAnimate.has(`data-${idx}`)) {\n rowEl.classList.add(animClass);\n rowEl.addEventListener('animationend', () => rowEl.classList.remove(animClass), { once: true });\n }\n }\n this.keysToAnimate.clear();\n }\n\n /**\n * Compute per-row ARIA hierarchy metadata (`aria-level` / `aria-setsize` /\n * `aria-posinset`) from `flattenedRows`. Two-pass over the flat list:\n * first pass tallies group siblings per (parent-prefix, depth) and counts\n * the data rows that follow each group header; second pass assigns per-row\n * meta. Called once per `flattenedRows` rebuild (not per render).\n */\n private computeFlatMeta(): void {\n const flat = this.flattenedRows;\n const meta: Array<{ level: number; setSize: number; posInSet: number }> = new Array(flat.length);\n\n // Pass 1: count group siblings per (parent-prefix, depth) and direct\n // data-row children per group (used as `setSize` for data rows).\n const groupSiblingCount = new Map<string, number>();\n const groupDataCount = new Map<string, number>();\n let activeGroup: GroupRowModelItem | null = null;\n let activeDataCount = 0;\n for (const item of flat) {\n if (item.kind === 'group') {\n if (activeGroup) groupDataCount.set(activeGroup.key, activeDataCount);\n activeGroup = item;\n activeDataCount = 0;\n const parts = item.key.split('||');\n const parent = parts.slice(0, -1).join('||');\n const k = `${parent}@${item.depth}`;\n groupSiblingCount.set(k, (groupSiblingCount.get(k) ?? 0) + 1);\n } else {\n activeDataCount++;\n }\n }\n if (activeGroup) groupDataCount.set(activeGroup.key, activeDataCount);\n\n // Pass 2: assign per-row level/setsize/posinset.\n const groupSiblingPos = new Map<string, number>();\n let currentGroup: GroupRowModelItem | null = null;\n let dataPos = 0;\n for (let i = 0; i < flat.length; i++) {\n const item = flat[i];\n if (item.kind === 'group') {\n const parts = item.key.split('||');\n const parent = parts.slice(0, -1).join('||');\n const k = `${parent}@${item.depth}`;\n const pos = (groupSiblingPos.get(k) ?? 0) + 1;\n groupSiblingPos.set(k, pos);\n meta[i] = { level: item.depth + 1, setSize: groupSiblingCount.get(k) ?? 1, posInSet: pos };\n currentGroup = item;\n dataPos = 0;\n } else {\n dataPos++;\n const baseDepth = currentGroup?.depth ?? -1;\n const setSize = currentGroup ? (groupDataCount.get(currentGroup.key) ?? 1) : flat.length;\n meta[i] = { level: baseDepth + 2, setSize, posInSet: dataPos };\n }\n }\n\n this.flatMeta = meta;\n }\n // #endregion\n\n // #region Pre-Defined Groups\n\n /**\n * Build the row model from pre-defined group definitions.\n * Used when `groups` config or `setGroups()` provides external group structure.\n */\n private processPreDefinedGroups(): any[] {\n const groups =\n this.preDefinedGroups.length > 0\n ? this.preDefinedGroups\n : Array.isArray(this.config.groups)\n ? this.config.groups\n : [];\n\n if (groups.length === 0) {\n this.isActive = false;\n this.flattenedRows = [];\n return [];\n }\n\n // Resolve defaultExpanded on first render only\n if (!this.hasAppliedDefaultExpanded && this.expandedKeys.size === 0 && this.config.defaultExpanded !== false) {\n const allKeys = this.collectGroupKeys(groups);\n const initialExpanded = resolveDefaultExpanded(this.config.defaultExpanded ?? false, allKeys);\n if (initialExpanded.size > 0) {\n this.expandedKeys = new Set(initialExpanded);\n this.hasAppliedDefaultExpanded = true;\n }\n }\n\n const grouped = buildPreDefinedGroupModel({\n groups,\n expanded: this.expandedKeys,\n groupRows: this.groupRowsMap,\n loadingGroups: this.loadingGroups,\n });\n\n this.isActive = true;\n this.flattenedRows = grouped;\n this.computeFlatMeta();\n\n // Track visible data rows for animation\n this.keysToAnimate.clear();\n const currentVisibleKeys = new Set<string>();\n grouped.forEach((item, idx) => {\n if (item.kind === 'data') {\n const key = `data-${idx}`;\n currentVisibleKeys.add(key);\n if (!this.previousVisibleKeys.has(key)) {\n this.keysToAnimate.add(key);\n }\n }\n });\n this.previousVisibleKeys = currentVisibleKeys;\n\n // Return flattened rows for rendering\n return grouped.map((item) => {\n if (item.kind === 'group') {\n // Look up the pre-defined group to get rowCount from server\n const groupDef = this.findGroupDefinition(groups, item.key);\n const rowCount = groupDef?.rowCount ?? item.rows.length;\n return {\n __isGroupRow: true,\n __groupKey: item.key,\n __groupValue: item.value,\n __groupDepth: item.depth,\n __groupRows: item.rows,\n __groupExpanded: item.expanded,\n __groupRowCount: rowCount,\n __rowCacheKey: `group:${item.key}`,\n };\n }\n return item.row;\n });\n }\n\n /**\n * Collect all group keys from a pre-defined group tree.\n */\n private collectGroupKeys(groups: GroupDefinition[]): string[] {\n const keys: string[] = [];\n for (const g of groups) {\n keys.push(g.key);\n if (g.children?.length) {\n keys.push(...this.collectGroupKeys(g.children));\n }\n }\n return keys;\n }\n\n /**\n * Get the active group definitions (pre-defined or empty).\n */\n private getActiveGroups(): GroupDefinition[] {\n if (this.preDefinedGroups.length > 0) return this.preDefinedGroups;\n return Array.isArray(this.config.groups) ? this.config.groups : [];\n }\n\n /**\n * Find a group definition by key in the pre-defined group tree.\n */\n private findGroupDefinition(groups: GroupDefinition[], key: string): GroupDefinition | undefined {\n for (const g of groups) {\n if (g.key === key) return g;\n if (g.children?.length) {\n const found = this.findGroupDefinition(g.children, key);\n if (found) return found;\n }\n }\n return undefined;\n }\n // #endregion\n\n // #region Private Rendering Helpers\n\n /**\n * Create a toggle button for expanding/collapsing a group.\n */\n private createToggleButton(expanded: boolean, handleToggle: () => void): HTMLButtonElement {\n const btn = document.createElement('button');\n btn.type = 'button';\n btn.className = `${GridClasses.GROUP_TOGGLE}${expanded ? ` ${GridClasses.EXPANDED}` : ''}`;\n btn.setAttribute('aria-label', expanded ? 'Collapse group' : 'Expand group');\n this.setIcon(btn, expanded ? 'collapse' : 'expand');\n btn.addEventListener('click', (e) => {\n e.stopPropagation();\n handleToggle();\n });\n return btn;\n }\n\n /**\n * Get the formatted label text for a group.\n */\n private getGroupLabelText(value: unknown, depth: number, key: string): string {\n const config = this.config;\n return config.formatLabel ? config.formatLabel(value, depth, key) : String(value);\n }\n\n private renderFullWidthGroupRow(row: any, rowEl: HTMLElement, handleToggle: () => void): void {\n const config = this.config;\n const aggregators = config.aggregators ?? {};\n const groupRows = row.__groupRows ?? [];\n\n // Full-width mode: single spanning cell with toggle + label + count + aggregates\n const cell = document.createElement('div');\n cell.className = 'cell group-full';\n cell.style.gridColumn = '1 / -1';\n cell.setAttribute('role', 'gridcell');\n cell.setAttribute('data-col', '0'); // Required for focus/click delegation\n\n // Toggle button\n cell.appendChild(this.createToggleButton(row.__groupExpanded, handleToggle));\n\n // Group label\n const label = document.createElement('span');\n label.className = GridClasses.GROUP_LABEL;\n label.textContent = this.getGroupLabelText(row.__groupValue, row.__groupDepth || 0, row.__groupKey);\n cell.appendChild(label);\n\n // Row count\n if (config.showRowCount !== false) {\n const count = document.createElement('span');\n count.className = GridClasses.GROUP_COUNT;\n count.textContent = `(${row.__groupRowCount ?? row.__groupRows?.length ?? 0})`;\n cell.appendChild(count);\n }\n\n // Render aggregates if configured\n const aggregatorEntries = Object.entries(aggregators);\n if (aggregatorEntries.length > 0) {\n const aggregatesContainer = document.createElement('span');\n aggregatesContainer.className = 'group-aggregates';\n\n for (const [field, aggRef] of aggregatorEntries) {\n const col = this.columns.find((c) => c.field === field);\n const result = aggregatorRegistry.run(aggRef, groupRows, field, col);\n if (result != null) {\n const aggSpan = document.createElement('span');\n aggSpan.className = 'group-aggregate';\n aggSpan.setAttribute('data-field', field);\n // Use column header as label if available\n const colHeader = col?.header ?? field;\n aggSpan.textContent = `${colHeader}: ${result}`;\n aggregatesContainer.appendChild(aggSpan);\n }\n }\n\n if (aggregatesContainer.children.length > 0) {\n cell.appendChild(aggregatesContainer);\n }\n }\n\n rowEl.appendChild(cell);\n }\n\n private renderPerColumnGroupRow(row: any, rowEl: HTMLElement, handleToggle: () => void): void {\n const config = this.config;\n const aggregators = config.aggregators ?? {};\n const columns = this.columns;\n const groupRows = row.__groupRows ?? [];\n\n // Get grid template from the grid element\n const bodyEl = this.gridElement?.querySelector('.body') as HTMLElement | null;\n const gridTemplate = bodyEl?.style.gridTemplateColumns || '';\n if (gridTemplate) {\n rowEl.style.display = 'grid';\n rowEl.style.gridTemplateColumns = gridTemplate;\n }\n\n // Track whether we've rendered the toggle button yet (should be in first non-expander column)\n let toggleRendered = false;\n\n columns.forEach((col, colIdx) => {\n const cell = document.createElement('div');\n cell.className = 'cell group-cell';\n cell.setAttribute('data-col', String(colIdx));\n cell.setAttribute('role', 'gridcell');\n\n // Skip expander columns (they're handled by other plugins like MasterDetail/Tree)\n // but still render an empty cell to maintain grid structure\n if (isExpanderColumn(col)) {\n cell.setAttribute('data-field', col.field);\n rowEl.appendChild(cell);\n return;\n }\n\n // First non-expander column gets the toggle button + label\n if (!toggleRendered) {\n toggleRendered = true;\n cell.appendChild(this.createToggleButton(row.__groupExpanded, handleToggle));\n\n const label = document.createElement('span');\n const firstColAgg = aggregators[col.field];\n if (firstColAgg) {\n const aggResult = aggregatorRegistry.run(firstColAgg, groupRows, col.field, col);\n label.textContent = aggResult != null ? String(aggResult) : String(row.__groupValue);\n } else {\n label.textContent = this.getGroupLabelText(row.__groupValue, row.__groupDepth || 0, row.__groupKey);\n }\n cell.appendChild(label);\n\n if (config.showRowCount !== false) {\n const count = document.createElement('span');\n count.className = GridClasses.GROUP_COUNT;\n count.textContent = ` (${groupRows.length})`;\n cell.appendChild(count);\n }\n } else {\n // Other columns: run aggregator if defined\n const aggRef = aggregators[col.field];\n if (aggRef) {\n const result = aggregatorRegistry.run(aggRef, groupRows, col.field, col);\n cell.textContent = result != null ? String(result) : '';\n } else {\n cell.textContent = '';\n }\n }\n\n rowEl.appendChild(cell);\n });\n }\n // #endregion\n\n // #region Public API\n\n /**\n * Expand all groups.\n *\n * Safe to call immediately after {@link setGroupOn}: when the grouping\n * config has just changed, the cached `flattenedRows` is stale, so the\n * call defers until the next render rebuilds the group model against the\n * new `groupOn` (issue #335). The `group-toggle` event is emitted once\n * the new keys are known.\n */\n expandAll(): void {\n if (this.groupConfigDirty) {\n // flattenedRows is stale — defer until processRows rebuilds it. Clear\n // the cached set so callers don't observe stale keys via\n // `getExpandedGroups()` between this call and the next render.\n this.pendingExpansion = true;\n this.expandedKeys = new Set();\n this.requestRender();\n return;\n }\n this.expandedKeys = expandAllGroups(this.flattenedRows);\n this.broadcast<GroupToggleDetail>('group-toggle', { expandedKeys: [...this.expandedKeys] });\n this.requestRender();\n }\n\n /**\n * Collapse all groups.\n *\n * Safe to call immediately after {@link setGroupOn}: defers until the next\n * render rebuilds the group model so the collapse targets the new groups\n * rather than a stale snapshot (issue #335).\n */\n collapseAll(): void {\n if (this.groupConfigDirty) {\n this.pendingExpansion = false;\n this.expandedKeys = new Set();\n this.requestRender();\n return;\n }\n this.expandedKeys = collapseAllGroups();\n this.broadcast<GroupToggleDetail>('group-toggle', { expandedKeys: [...this.expandedKeys] });\n this.requestRender();\n }\n\n /**\n * Toggle expansion of a specific group.\n * In accordion mode, expanding a group will collapse all sibling groups.\n * @param key - The group key to toggle\n */\n toggle(key: string): void {\n const isExpanding = !this.expandedKeys.has(key);\n const config = this.config;\n\n // Find the group to get its depth for accordion mode\n const group = this.flattenedRows.find((r) => r.kind === 'group' && r.key === key) as GroupRowModelItem | undefined;\n\n // In accordion mode, collapse sibling groups when expanding\n if (config.accordion && isExpanding && group) {\n const newKeys = new Set<string>();\n // Keep only ancestors (keys that are prefixes of the current key) and the current key\n for (const existingKey of this.expandedKeys) {\n // Check if existingKey is an ancestor of the toggled key\n // Ancestors have composite keys that are prefixes of child keys (separated by '||')\n if (key.startsWith(existingKey + '||') || existingKey.startsWith(key + '||')) {\n // This is an ancestor or descendant - keep it only if ancestor\n if (key.startsWith(existingKey + '||')) {\n newKeys.add(existingKey);\n }\n } else {\n // Check depth - only keep groups at different depths\n const existingGroup = this.flattenedRows.find((r) => r.kind === 'group' && r.key === existingKey) as\n GroupRowModelItem | undefined;\n if (existingGroup && existingGroup.depth !== group.depth) {\n newKeys.add(existingKey);\n }\n }\n }\n newKeys.add(key);\n this.expandedKeys = newKeys;\n } else {\n this.expandedKeys = toggleGroupExpansion(this.expandedKeys, key);\n }\n\n this.broadcast<GroupToggleDetail>('group-toggle', {\n key,\n expanded: this.expandedKeys.has(key),\n value: group?.value,\n depth: group?.depth ?? 0,\n expandedKeys: [...this.expandedKeys],\n });\n\n // Emit group-expand/group-collapse events for pre-defined mode\n const activeGroups = this.getActiveGroups();\n if (activeGroups.length > 0) {\n const groupPath = getGroupPath(activeGroups, key);\n if (isExpanding) {\n this.emit<GroupExpandDetail>('group-expand', { groupKey: key, groupPath });\n\n // Request child rows via unified DataSource if not already cached\n if (!this.groupRowsMap.has(key)) {\n const groupDef = this.findGroupDefinition(activeGroups, key);\n if (groupDef) {\n this.loadingGroups.add(key);\n this.grid?.query?.('datasource:fetch-children', {\n context: { source: 'grouping-rows', groupKey: key, group: groupDef, groupPath },\n } satisfies FetchChildrenQuery);\n }\n }\n } else {\n this.emit<GroupCollapseDetail>('group-collapse', { groupKey: key, groupPath });\n }\n }\n\n // Announce group state change for screen readers\n const expanded = this.expandedKeys.has(key);\n const groupName = group?.value != null ? String(group.value) : key;\n if (expanded) {\n const rowCount = group?.rows?.length ?? 0;\n announce(this.gridElement, getA11yMessage(this.gridElement, 'groupExpanded', groupName, rowCount));\n } else {\n announce(this.gridElement, getA11yMessage(this.gridElement, 'groupCollapsed', groupName));\n }\n\n this.requestRender();\n }\n\n /**\n * Check if a specific group is expanded.\n * @param key - The group key to check\n * @returns Whether the group is expanded\n */\n isExpanded(key: string): boolean {\n return this.expandedKeys.has(key);\n }\n\n /**\n * Expand a specific group.\n * @param key - The group key to expand\n */\n expand(key: string): void {\n if (!this.expandedKeys.has(key)) {\n this.expandedKeys = new Set([...this.expandedKeys, key]);\n this.requestRender();\n }\n }\n\n /**\n * Collapse a specific group.\n * @param key - The group key to collapse\n */\n collapse(key: string): void {\n if (this.expandedKeys.has(key)) {\n const newKeys = new Set(this.expandedKeys);\n newKeys.delete(key);\n this.expandedKeys = newKeys;\n this.requestRender();\n }\n }\n\n /**\n * Get the current group state.\n * @returns Group state information\n */\n getGroupState(): GroupState {\n const groupRows = this.flattenedRows.filter((r) => r.kind === 'group');\n return {\n isActive: this.isActive,\n expandedCount: this.expandedKeys.size,\n totalGroups: groupRows.length,\n expandedKeys: [...this.expandedKeys],\n };\n }\n\n /**\n * Get the total count of visible rows (including group headers).\n * @returns Number of visible rows\n */\n getRowCount(): number {\n return this.flattenedRows.length;\n }\n\n /**\n * Refresh the grouped row model.\n * Call this after modifying groupOn or other config options.\n */\n refreshGroups(): void {\n this.requestRender();\n }\n\n /**\n * Get current expanded group keys.\n * @returns Array of expanded group keys\n */\n getExpandedGroups(): string[] {\n return [...this.expandedKeys];\n }\n\n /**\n * Get the flattened row model.\n * @returns Array of render rows (groups + data rows)\n */\n getFlattenedRows(): RenderRow[] {\n return this.flattenedRows;\n }\n\n /**\n * Check if grouping is currently active.\n * @returns Whether grouping is active\n */\n isGroupingActive(): boolean {\n return this.isActive;\n }\n\n /**\n * Set the groupOn function dynamically.\n *\n * Optionally specify the initial expansion state to apply against the new\n * group set. Without it, the previous `expandedKeys` are kept (and most\n * likely won't match the new keys, so groups land collapsed). With it,\n * the new groups are expanded synchronously on the next render — no need\n * to chain `setGroupOn(fn); expandAll()` or wait for `requestAnimationFrame`.\n *\n * If you do call `expandAll()` / `collapseAll()` separately right after\n * `setGroupOn`, those calls automatically defer to the same mechanism so\n * they target the new groups rather than the stale snapshot. See issue #335.\n *\n * @param fn - The groupOn function or undefined to disable\n * @param expanded - Optional initial expansion for the new grouping:\n * - `true`: expand all new groups\n * - `false`: collapse all new groups\n * - `string` / `string[]`: expand specific group key(s)\n * - `number`: expand the group at this index\n * When omitted, existing `expandedKeys` are preserved (legacy behavior).\n *\n * @example Group by a new field and expand everything\n * ```ts\n * plugin.setGroupOn((row) => row.counterparty, true);\n * ```\n *\n * @example Group by a new field but keep all groups collapsed\n * ```ts\n * plugin.setGroupOn((row) => row.region, false);\n * ```\n */\n setGroupOn(fn: ((row: any) => any[] | any | null | false) | undefined, expanded?: DefaultExpandedValue): void {\n (this.config as GroupingRowsConfig).groupOn = fn;\n this.groupConfigDirty = true;\n if (expanded !== undefined) {\n this.pendingExpansion = expanded;\n this.expandedKeys = new Set();\n this.hasAppliedDefaultExpanded = false;\n }\n this.requestRender();\n }\n\n // --- Pre-defined group API ---\n\n /**\n * Replace auto-detected groups with an externally provided group structure.\n *\n * When groups are set, the plugin switches to pre-defined mode — `groupOn`\n * is ignored and the plugin renders the provided group headers instead.\n * Row data for each group must be populated via {@link setGroupRows}.\n *\n * @param groups - Array of group definitions, or empty array to clear\n */\n setGroups(groups: GroupDefinition[]): void {\n this.preDefinedGroups = groups;\n this.groupRowsMap.clear();\n this.loadingGroups.clear();\n this.expandedKeys.clear();\n this.hasAppliedDefaultExpanded = false;\n this.requestRender();\n }\n\n /**\n * Get the current pre-defined group structure.\n *\n * Returns the groups set via {@link setGroups}, received from `datasource:data`,\n * or the `groups` config array. Returns an empty array when using `groupOn`-based grouping.\n *\n * @returns Current group definitions\n */\n getGroups(): GroupDefinition[] {\n if (this.preDefinedGroups.length > 0) return [...this.preDefinedGroups];\n return Array.isArray(this.config.groups) ? [...this.config.groups] : [];\n }\n\n /**\n * Populate row data for an expanded group.\n *\n * Call this in response to a `group-expand` event after fetching rows\n * from the server. The plugin will re-render to show the rows.\n *\n * When `ServerSidePlugin` is loaded, this is handled automatically via\n * `datasource:children` events — you only need this for the imperative API.\n *\n * @param groupKey - The group key to populate\n * @param rows - The row data for this group\n */\n setGroupRows(groupKey: string, rows: unknown[]): void {\n this.groupRowsMap.set(groupKey, rows);\n this.loadingGroups.delete(groupKey);\n this.requestRender();\n }\n\n /**\n * Toggle loading indicator for a group.\n *\n * When loading is true, the group shows a loading spinner instead of row data.\n * Call with `false` after rows are loaded (also cleared by {@link setGroupRows}).\n *\n * @param groupKey - The group key\n * @param loading - Whether the group is loading\n */\n setGroupLoading(groupKey: string, loading: boolean): void {\n if (loading) {\n this.loadingGroups.add(groupKey);\n } else {\n this.loadingGroups.delete(groupKey);\n }\n this.requestRender();\n }\n\n /**\n * Clear cached row data for one or all groups.\n *\n * Use when the server data has changed and groups need to be re-fetched\n * on next expand.\n *\n * @param groupKey - Specific group key to clear, or omit to clear all\n */\n clearGroupRows(groupKey?: string): void {\n if (groupKey != null) {\n this.groupRowsMap.delete(groupKey);\n } else {\n this.groupRowsMap.clear();\n }\n this.requestRender();\n }\n // #endregion\n}\n"],"mappings":"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"}