@toolbox-web/grid 3.8.3 → 3.9.0
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.
- package/all.d.ts +1 -0
- package/all.js +1 -1
- package/all.js.map +1 -1
- package/index.js +1 -1
- package/index.js.map +1 -1
- package/lib/core/plugin/base-plugin.d.ts +14 -0
- package/lib/core/plugin/plugin-manager.d.ts +5 -1
- package/lib/core/plugin/types.d.ts +2 -0
- package/lib/features/registry.js +1 -1
- package/lib/plugins/clipboard/index.js +1 -1
- package/lib/plugins/clipboard/index.js.map +1 -1
- package/lib/plugins/column-virtualization/index.js +1 -1
- package/lib/plugins/column-virtualization/index.js.map +1 -1
- package/lib/plugins/context-menu/index.js +1 -1
- package/lib/plugins/context-menu/index.js.map +1 -1
- package/lib/plugins/editing/index.js +1 -1
- package/lib/plugins/editing/index.js.map +1 -1
- package/lib/plugins/export/index.js +1 -1
- package/lib/plugins/export/index.js.map +1 -1
- package/lib/plugins/filtering/index.js +1 -1
- package/lib/plugins/filtering/index.js.map +1 -1
- package/lib/plugins/grouping-columns/index.js +1 -1
- package/lib/plugins/grouping-columns/index.js.map +1 -1
- package/lib/plugins/grouping-rows/grouping-rows-plugin.d.ts +20 -24
- package/lib/plugins/grouping-rows/grouping-rows.d.ts +9 -4
- package/lib/plugins/grouping-rows/index.d.ts +1 -1
- package/lib/plugins/grouping-rows/index.js +1 -1
- package/lib/plugins/grouping-rows/index.js.map +1 -1
- package/lib/plugins/grouping-rows/types.d.ts +32 -4
- package/lib/plugins/hierarchy/hierarchy-plugin.d.ts +79 -0
- package/lib/plugins/hierarchy/hierarchy.d.ts +28 -0
- package/lib/plugins/hierarchy/index.d.ts +9 -0
- package/lib/plugins/hierarchy/index.js +2 -0
- package/lib/plugins/hierarchy/index.js.map +1 -0
- package/lib/plugins/hierarchy/types.d.ts +92 -0
- package/lib/plugins/master-detail/index.js +1 -1
- package/lib/plugins/master-detail/index.js.map +1 -1
- package/lib/plugins/multi-sort/index.js +1 -1
- package/lib/plugins/multi-sort/index.js.map +1 -1
- package/lib/plugins/pinned-columns/index.js +1 -1
- package/lib/plugins/pinned-columns/index.js.map +1 -1
- package/lib/plugins/pinned-rows/index.js +1 -1
- package/lib/plugins/pinned-rows/index.js.map +1 -1
- package/lib/plugins/pivot/index.js +1 -1
- package/lib/plugins/pivot/index.js.map +1 -1
- package/lib/plugins/print/index.js +1 -1
- package/lib/plugins/print/index.js.map +1 -1
- package/lib/plugins/reorder-columns/index.js +1 -1
- package/lib/plugins/reorder-columns/index.js.map +1 -1
- package/lib/plugins/responsive/index.js +1 -1
- package/lib/plugins/responsive/index.js.map +1 -1
- package/lib/plugins/row-drag-drop/index.js +1 -1
- package/lib/plugins/row-drag-drop/index.js.map +1 -1
- package/lib/plugins/selection/index.js +1 -1
- package/lib/plugins/selection/index.js.map +1 -1
- package/lib/plugins/server-side/index.js +1 -1
- package/lib/plugins/server-side/index.js.map +1 -1
- package/lib/plugins/shell/index.js +1 -1
- package/lib/plugins/shell/index.js.map +1 -1
- package/lib/plugins/sticky-rows/index.js +1 -1
- package/lib/plugins/sticky-rows/index.js.map +1 -1
- package/lib/plugins/tooltip/index.js +1 -1
- package/lib/plugins/tooltip/index.js.map +1 -1
- package/lib/plugins/tree/index.js +1 -1
- package/lib/plugins/tree/index.js.map +1 -1
- package/lib/plugins/tree/tree-datasource.d.ts +7 -10
- package/lib/plugins/tree/tree-plugin.d.ts +23 -3
- package/lib/plugins/undo-redo/index.js +1 -1
- package/lib/plugins/undo-redo/index.js.map +1 -1
- package/lib/plugins/visibility/index.js +1 -1
- package/lib/plugins/visibility/index.js.map +1 -1
- package/package.json +1 -1
- package/umd/grid.all.umd.js +1 -1
- package/umd/grid.all.umd.js.map +1 -1
- package/umd/grid.umd.js +1 -1
- package/umd/grid.umd.js.map +1 -1
- package/umd/plugins/grouping-rows.umd.js +1 -1
- package/umd/plugins/grouping-rows.umd.js.map +1 -1
- package/umd/plugins/hierarchy.umd.js +2 -0
- package/umd/plugins/hierarchy.umd.js.map +1 -0
- package/umd/plugins/selection.umd.js +1 -1
- package/umd/plugins/selection.umd.js.map +1 -1
- package/umd/plugins/tree.umd.js +1 -1
- package/umd/plugins/tree.umd.js.map +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../../../../../libs/grid/src/lib/core/internal/diagnostics.ts","../../../../../../libs/grid/src/lib/core/constants.ts","../../../../../../libs/grid/src/lib/core/internal/utils.ts","../../../../../../libs/grid/src/lib/core/internal/virtualization.ts","../../../../../../libs/grid/src/lib/core/internal/keyboard.ts","../../../../../../libs/grid/src/lib/core/internal/sanitize.ts","../../../../../../libs/grid/src/lib/core/defaults.ts","../../../../../../libs/grid/src/lib/core/plugin/base-plugin.ts","../../../../../../libs/grid/src/lib/plugins/responsive/responsive.css?inline","../../../../../../libs/grid/src/lib/plugins/responsive/responsive-plugin.ts"],"sourcesContent":["/**\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 * 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","// #region Environment Helpers\n\n/**\n * Check if we're running in a development environment.\n * Returns true for localhost or when NODE_ENV !== 'production'.\n * Used to show warnings only in development.\n */\nexport function isDevelopment(): boolean {\n // Check for localhost (browser environment)\n if (typeof window !== 'undefined' && window.location) {\n const hostname = window.location.hostname;\n if (hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1') {\n return true;\n }\n }\n // Check for NODE_ENV (build-time or SSR)\n if (typeof process !== 'undefined' && process.env?.NODE_ENV !== 'production') {\n return true;\n }\n return false;\n}\n\n// #endregion\n\n// #region Cell Rendering Helpers\n\n/**\n * Generate accessible HTML for a boolean cell.\n * Uses role=\"checkbox\" with proper aria attributes.\n */\nexport function booleanCellHTML(value: boolean): string {\n return `<span role=\"checkbox\" aria-checked=\"${value}\" aria-label=\"${value}\">${value ? '🗹' : '☐'}</span>`;\n}\n\n/**\n * Format a date value for display.\n * Handles Date objects, timestamps, and date strings.\n * Returns empty string for invalid dates.\n */\nexport function formatDateValue(value: unknown): string {\n if (value == null || value === '') return '';\n if (value instanceof Date) {\n return isNaN(value.getTime()) ? '' : value.toLocaleDateString();\n }\n if (typeof value === 'number' || typeof value === 'string') {\n const d = new Date(value);\n return isNaN(d.getTime()) ? '' : d.toLocaleDateString();\n }\n return '';\n}\n\n/**\n * Get the row index from a cell element's data-row attribute.\n * Falls back to calculating from parent row's DOM position if data-row is missing.\n * Returns -1 if no valid row index is found.\n */\nexport function getRowIndexFromCell(cell: Element | null): number {\n if (!cell) return -1;\n const attr = cell.getAttribute('data-row');\n if (attr) return parseInt(attr, 10);\n\n // Fallback: find the parent .data-grid-row and calculate index from siblings\n const rowEl = cell.closest('.data-grid-row');\n if (!rowEl) return -1;\n\n const parent = rowEl.parentElement;\n if (!parent) return -1;\n\n // Get all data-grid-row siblings and find this row's index\n const rows = parent.querySelectorAll(':scope > .data-grid-row');\n for (let i = 0; i < rows.length; i++) {\n if (rows[i] === rowEl) return i;\n }\n return -1;\n}\n\n/**\n * Get the column index from a cell element's data-col attribute.\n * Returns -1 if no valid column index is found.\n */\nexport function getColIndexFromCell(cell: Element | null): number {\n if (!cell) return -1;\n const attr = cell.getAttribute('data-col');\n return attr ? parseInt(attr, 10) : -1;\n}\n\n/**\n * Clear all cell-focus styling from a root element.\n * Used when changing focus or when selection plugin takes over focus management.\n */\nexport function clearCellFocus(root: Element | null): void {\n if (!root) return;\n root.querySelectorAll('.cell-focus').forEach((el) => el.classList.remove('cell-focus'));\n}\n// #endregion\n\n// #region RTL Helpers\n\n/** Text direction */\nexport type TextDirection = 'ltr' | 'rtl';\n\n/**\n * Get the text direction for an element.\n * Reads from the computed style, which respects the `dir` attribute on the element\n * or any ancestor, as well as CSS `direction` property.\n *\n * @param element - The element to check direction for\n * @returns 'ltr' or 'rtl'\n *\n * @example\n * ```typescript\n * // Detect grid's direction\n * const dir = getDirection(gridElement);\n * if (dir === 'rtl') {\n * // Handle RTL layout\n * }\n * ```\n */\nexport function getDirection(element: Element): TextDirection {\n // Try computed style first (works in real browsers)\n try {\n const computedDir = getComputedStyle(element).direction;\n if (computedDir === 'rtl') return 'rtl';\n } catch {\n // getComputedStyle may fail in some test environments\n }\n\n // Fallback: check dir attribute on element or ancestors\n // This handles test environments where getComputedStyle may not reflect dir attribute\n try {\n const dirAttr = element.closest?.('[dir]')?.getAttribute('dir');\n if (dirAttr === 'rtl') return 'rtl';\n } catch {\n // closest may not be available on mock elements\n }\n\n return 'ltr';\n}\n\n/**\n * Check if an element is in RTL mode.\n *\n * @param element - The element to check\n * @returns true if the element's text direction is right-to-left\n */\nexport function isRTL(element: Element): boolean {\n return getDirection(element) === 'rtl';\n}\n\n/**\n * Resolve a logical inline position to a physical position based on text direction.\n *\n * - `'start'` → `'left'` in LTR, `'right'` in RTL\n * - `'end'` → `'right'` in LTR, `'left'` in RTL\n * - `'left'` / `'right'` → unchanged (physical values)\n *\n * @param position - Logical or physical position\n * @param direction - Text direction ('ltr' or 'rtl')\n * @returns Physical position ('left' or 'right')\n *\n * @example\n * ```typescript\n * resolveInlinePosition('start', 'ltr'); // 'left'\n * resolveInlinePosition('start', 'rtl'); // 'right'\n * resolveInlinePosition('left', 'rtl'); // 'left' (unchanged)\n * ```\n */\nexport function resolveInlinePosition(\n position: 'left' | 'right' | 'start' | 'end',\n direction: TextDirection,\n): 'left' | 'right' {\n if (position === 'left' || position === 'right') {\n return position;\n }\n if (direction === 'rtl') {\n return position === 'start' ? 'right' : 'left';\n }\n return position === 'start' ? 'left' : 'right';\n}\n// #endregion\n","/**\n * Row Virtualization Module\n *\n * Provides all virtualization-related functionality for the grid:\n *\n * 1. **Position Cache** (index-based): Maps row index → {offset, height, measured}\n * - Rebuilt when row count changes (expand/collapse, filter)\n * - Used for scroll position → row index lookups (binary search)\n *\n * 2. **Height Cache** (identity-based): Maps row identity → height\n * - Persists across expand/collapse\n * - Uses rowId string keys when available, WeakMap otherwise\n * - Synthetic rows use __rowCacheKey for stable identity\n *\n * 3. **Virtual Window**: Computes which rows to render based on scroll position\n *\n * 4. **Row Measurement**: Measures rendered row heights from DOM\n */\n\n// #region Types\n\n/**\n * Position entry for a single row in the position cache.\n * @category Plugin Development\n */\nexport interface RowPosition {\n /** Cumulative offset from top in pixels */\n offset: number;\n /** Row height in pixels */\n height: number;\n /** Whether this is a measured value (true) or estimate (false) */\n measured: boolean;\n}\n\n/**\n * Height cache that persists row heights across position cache rebuilds.\n * Uses dual storage: Map for string keys (rowId, __rowCacheKey) and WeakMap for object refs.\n * @category Plugin Development\n */\nexport interface HeightCache {\n /** Heights keyed by string (for synthetic rows with __rowCacheKey or rowId-keyed rows) */\n byKey: Map<string, number>;\n /** Heights keyed by object reference (for data rows without rowId) */\n byRef: WeakMap<object, number>;\n}\n\n/**\n * Configuration for the position cache.\n */\nexport interface PositionCacheConfig<T = unknown> {\n /** Function to get row ID (if configured) */\n rowId?: (row: T) => string | number;\n}\n\n// #endregion\n\n// #region Height Cache Operations\n\n/**\n * Create a new empty height cache.\n */\nexport function createHeightCache(): HeightCache {\n return {\n byKey: new Map(),\n byRef: new WeakMap(),\n };\n}\n\n/**\n * Get the cache key for a row.\n * Returns string for synthetic rows (__rowCacheKey) or rowId-keyed rows,\n * or the row object itself for WeakMap lookup.\n */\nexport function getRowCacheKey<T>(row: T, rowId?: (row: T) => string | number): string | T {\n if (!row || typeof row !== 'object') return row;\n\n // 1. Synthetic rows: plugins MUST add __rowCacheKey\n if ('__rowCacheKey' in row) {\n return (row as { __rowCacheKey: string }).__rowCacheKey;\n }\n\n // 2. rowId property directly on the row (common pattern)\n if ('rowId' in row && (row as { rowId: string | number }).rowId != null) {\n return `id:${(row as { rowId: string | number }).rowId}`;\n }\n\n // 3. User-provided rowId function (if configured)\n if (rowId) {\n return `id:${rowId(row)}`;\n }\n\n // 4. Object identity (for WeakMap)\n return row;\n}\n\n/**\n * Get cached height for a row.\n * @returns Cached height or undefined if not cached\n */\nexport function getCachedHeight<T>(\n cache: HeightCache,\n row: T,\n rowId?: (row: T) => string | number,\n): number | undefined {\n const key = getRowCacheKey(row, rowId);\n\n if (typeof key === 'string') {\n return cache.byKey.get(key);\n }\n\n // Object key - use WeakMap\n if (key && typeof key === 'object') {\n return cache.byRef.get(key);\n }\n\n return undefined;\n}\n\n/**\n * Set cached height for a row.\n */\nexport function setCachedHeight<T>(\n cache: HeightCache,\n row: T,\n height: number,\n rowId?: (row: T) => string | number,\n): void {\n const key = getRowCacheKey(row, rowId);\n\n if (typeof key === 'string') {\n cache.byKey.set(key, height);\n } else if (key && typeof key === 'object') {\n cache.byRef.set(key, height);\n }\n}\n\n// #endregion\n\n// #region Position Cache Operations\n\n/**\n * Rebuild position cache preserving known heights from height cache.\n * Called when row count changes (expand/collapse, filter, data change).\n *\n * @param rows - Array of row data\n * @param heightCache - Height cache with persisted measurements\n * @param estimatedHeight - Estimated height for unmeasured rows\n * @param config - Position cache configuration\n * @param getPluginHeight - Optional function to get height from plugins\n * @returns New position cache\n */\nexport function rebuildPositionCache<T>(\n rows: T[],\n heightCache: HeightCache,\n estimatedHeight: number,\n config: PositionCacheConfig<T>,\n getPluginHeight?: (row: T, index: number) => number | undefined,\n): RowPosition[] {\n const cache: RowPosition[] = new Array(rows.length);\n let offset = 0;\n\n for (let i = 0; i < rows.length; i++) {\n const row = rows[i];\n\n // Height resolution order:\n // 1. Plugin's getRowHeight() (for synthetic rows with known heights)\n let height = getPluginHeight?.(row, i);\n let measured = height !== undefined;\n\n // 2. Cached height from previous measurements\n if (height === undefined) {\n height = getCachedHeight(heightCache, row, config.rowId);\n measured = height !== undefined;\n }\n\n // 3. Fall back to estimate\n if (height === undefined) {\n height = estimatedHeight;\n measured = false;\n }\n\n cache[i] = { offset, height, measured };\n offset += height;\n }\n\n return cache;\n}\n\n/**\n * Update a single row's height in the position cache.\n * Recalculates offsets for all subsequent rows.\n *\n * @param cache - Position cache to update\n * @param index - Row index to update\n * @param newHeight - New measured height\n */\nexport function updateRowHeight(cache: RowPosition[], index: number, newHeight: number): void {\n if (index < 0 || index >= cache.length) return;\n\n const entry = cache[index];\n const heightDiff = newHeight - entry.height;\n\n // Latch `measured` even when the height is unchanged, otherwise callers that enqueue on\n // `!measured` re-measure the same row on every render pass.\n entry.measured = true;\n\n if (heightDiff === 0) return;\n\n // Update this row\n entry.height = newHeight;\n\n // Recalculate offsets for all subsequent rows\n for (let i = index + 1; i < cache.length; i++) {\n cache[i].offset += heightDiff;\n }\n}\n\nexport interface RowHeightChange {\n /** Row index into the position cache. */\n index: number;\n /** Measured height in pixels. */\n height: number;\n}\n\n/**\n * Update multiple row heights while recalculating the affected cache suffix once.\n *\n * @param cache - Position cache to update (mutated in place)\n * @param changes - Height changes in any order; each row index should occur at most once\n */\nexport function updateRowHeights(cache: RowPosition[], changes: RowHeightChange[]): void {\n if (changes.length === 0 || cache.length === 0) return;\n\n // Callers usually collect in DOM order, which is already ascending — skip the copy then.\n let sortedChanges = changes;\n for (let i = 1; i < changes.length; i++) {\n if (changes[i].index < changes[i - 1].index) {\n sortedChanges = [...changes].sort((a, b) => a.index - b.index);\n break;\n }\n }\n\n let changeIndex = 0;\n while (changeIndex < sortedChanges.length && sortedChanges[changeIndex].index < 0) changeIndex++;\n if (changeIndex >= sortedChanges.length || sortedChanges[changeIndex].index >= cache.length) return;\n\n let cumulativeDiff = 0;\n const firstChangedIndex = sortedChanges[changeIndex].index;\n for (let index = firstChangedIndex; index < cache.length; index++) {\n const entry = cache[index];\n entry.offset += cumulativeDiff;\n\n while (changeIndex < sortedChanges.length && sortedChanges[changeIndex].index === index) {\n const height = sortedChanges[changeIndex].height;\n const heightDiff = height - entry.height;\n entry.measured = true;\n if (heightDiff !== 0) {\n entry.height = height;\n cumulativeDiff += heightDiff;\n }\n changeIndex++;\n }\n }\n}\n\n/**\n * Get total content height from position cache.\n *\n * @param cache - Position cache\n * @returns Total height in pixels\n */\nexport function getTotalHeight(cache: RowPosition[]): number {\n if (cache.length === 0) return 0;\n const last = cache[cache.length - 1];\n return last.offset + last.height;\n}\n\n// #endregion\n\n// #region Binary Search\n\n/**\n * Find the row index at a given scroll offset using binary search.\n * Returns the index of the row that contains the given pixel offset.\n *\n * @param cache - Position cache\n * @param targetOffset - Scroll offset in pixels\n * @returns Row index at that offset, or -1 if cache is empty\n */\nexport function getRowIndexAtOffset(cache: RowPosition[], targetOffset: number): number {\n if (cache.length === 0) return -1;\n if (targetOffset <= 0) return 0;\n\n let low = 0;\n let high = cache.length - 1;\n\n while (low <= high) {\n const mid = Math.floor((low + high) / 2);\n const entry = cache[mid];\n const entryEnd = entry.offset + entry.height;\n\n if (targetOffset < entry.offset) {\n high = mid - 1;\n } else if (targetOffset >= entryEnd) {\n low = mid + 1;\n } else {\n // targetOffset is within this row\n return mid;\n }\n }\n\n // Return the closest row (low will be just past the target)\n return Math.max(0, Math.min(low, cache.length - 1));\n}\n\n// #endregion\n\n// #region Statistics\n\n/**\n * Calculate the average measured height.\n * Used for estimating unmeasured rows.\n *\n * @param cache - Position cache\n * @param defaultHeight - Default height to use if no measurements\n * @returns Average measured height\n */\nexport function calculateAverageHeight(cache: RowPosition[], defaultHeight: number): number {\n let totalHeight = 0;\n let measuredCount = 0;\n\n for (const entry of cache) {\n if (entry.measured) {\n totalHeight += entry.height;\n measuredCount++;\n }\n }\n\n return measuredCount > 0 ? totalHeight / measuredCount : defaultHeight;\n}\n\n/**\n * Count how many rows have been measured.\n *\n * @param cache - Position cache\n * @returns Number of measured rows\n */\nexport function countMeasuredRows(cache: RowPosition[]): number {\n let count = 0;\n for (const entry of cache) {\n if (entry.measured) count++;\n }\n return count;\n}\n\n// #endregion\n// #region Row Measurement\n\n/**\n * Result of measuring rendered row heights.\n */\nexport interface RowMeasurementResult {\n /** Whether any heights changed */\n hasChanges: boolean;\n /** Updated measured row count */\n measuredCount: number;\n /** Updated average height */\n averageHeight: number;\n}\n\n/**\n * Context for measuring rendered rows.\n */\nexport interface RowMeasurementContext<T> {\n /** Position cache to update */\n positionCache: RowPosition[];\n /** Height cache for persistence */\n heightCache: HeightCache;\n /** Row data array */\n rows: T[];\n /** Default row height */\n defaultHeight: number;\n /** Start index of rendered window */\n start: number;\n /** End index of rendered window (exclusive) */\n end: number;\n /** Function to get plugin height for a row */\n getPluginHeight?: (row: T, index: number) => number | undefined;\n /** Function to get row ID for height cache keying */\n getRowId?: (row: T) => string | number;\n}\n\n/**\n * Measure rendered row heights from DOM elements and update caches.\n * Returns measurement statistics for updating virtualization state.\n *\n * @param context - Measurement context with all dependencies\n * @param rowElements - NodeList of rendered row elements\n * @returns Measurement result with flags and statistics\n */\nexport function measureRenderedRowHeights<T>(\n context: RowMeasurementContext<T>,\n rowElements: NodeListOf<Element>,\n): RowMeasurementResult {\n const { positionCache, heightCache, rows, start, end, getPluginHeight, getRowId } = context;\n\n let hasChanges = false;\n const heightChanges: RowHeightChange[] = [];\n\n rowElements.forEach((rowEl) => {\n const rowIndexStr = (rowEl as HTMLElement).dataset.rowIndex;\n if (!rowIndexStr) return;\n\n const rowIndex = parseInt(rowIndexStr, 10);\n if (rowIndex < start || rowIndex >= end || rowIndex >= rows.length) return;\n\n const row = rows[rowIndex];\n\n // Check if a plugin provides the height for this row\n const pluginHeight = getPluginHeight?.(row, rowIndex);\n\n if (pluginHeight !== undefined) {\n // Plugin provides height - use it for position cache\n const currentEntry = positionCache[rowIndex];\n if (!currentEntry.measured || Math.abs(currentEntry.height - pluginHeight) > 1) {\n heightChanges.push({ index: rowIndex, height: pluginHeight });\n hasChanges = true;\n }\n return; // Don't measure DOM for plugin-managed rows\n }\n\n // No plugin height - use DOM measurement\n const measuredHeight = (rowEl as HTMLElement).offsetHeight;\n\n if (measuredHeight > 0) {\n const currentEntry = positionCache[rowIndex];\n\n // Only update if height differs significantly (> 1px to avoid oscillation)\n if (!currentEntry.measured || Math.abs(currentEntry.height - measuredHeight) > 1) {\n heightChanges.push({ index: rowIndex, height: measuredHeight });\n setCachedHeight(heightCache, row, measuredHeight, getRowId);\n hasChanges = true;\n }\n }\n });\n\n updateRowHeights(positionCache, heightChanges);\n\n // Recompute stats\n const measuredCount = hasChanges ? countMeasuredRows(positionCache) : 0;\n const averageHeight = hasChanges ? calculateAverageHeight(positionCache, context.defaultHeight) : 0;\n\n return { hasChanges, measuredCount, averageHeight };\n}\n\n/**\n * Compute measurement statistics for a position cache, excluding plugin-managed rows.\n * Used after rebuilding position cache to get accurate averages for non-plugin rows.\n *\n * @param cache - Position cache\n * @param rows - Row data array\n * @param defaultHeight - Default height if no measurements\n * @param getPluginHeight - Function to check if plugin manages row height\n * @returns Object with measured count and average height\n */\nexport function computeAverageExcludingPluginRows<T>(\n cache: RowPosition[],\n rows: T[],\n defaultHeight: number,\n getPluginHeight?: (row: T, index: number) => number | undefined,\n): { measuredCount: number; averageHeight: number } {\n let measuredCount = 0;\n let totalMeasured = 0;\n\n for (let i = 0; i < cache.length; i++) {\n const entry = cache[i];\n if (entry.measured) {\n // Only include in average if plugin doesn't provide height for this row\n const pluginHeight = getPluginHeight?.(rows[i], i);\n if (pluginHeight === undefined) {\n totalMeasured += entry.height;\n measuredCount++;\n }\n }\n }\n\n return {\n measuredCount,\n averageHeight: measuredCount > 0 ? totalMeasured / measuredCount : defaultHeight,\n };\n}\n\n// #endregion\n\n// #region Scroll Mapping (max element-height cap)\n\n/**\n * Maximum spacer element height (px) before browsers cap the rendered height.\n *\n * Chromium's hard cap is `2^25 = 33,554,432` px; Firefox/Safari are similar or larger.\n * We pick `33,500,000` — ~54 KB of headroom under Chromium's cap so that floating-point\n * drift in the fractional mapping math (`scrollTop * rMax / sMax`) cannot push a computed\n * spacer position past the engine cap and silently truncate.\n *\n * For datasets where `totalRows * rowHeight` exceeds this value, the faux-vscroll\n * spacer would silently truncate, making the tail of the dataset unreachable via\n * the native scrollbar / `Ctrl+End`. The grid switches to fractional scroll mapping\n * above this threshold — see {@link computeScrollMapping}.\n *\n * @category Plugin Development\n * @since 2.13.0\n */\nexport const MAX_ELEMENT_HEIGHT_PX = 33_500_000;\n\n/**\n * Mapping between native `scrollTop` (clamped spacer space) and \"virtual\" scroll\n * position (raw row-content space). See {@link computeScrollMapping}.\n *\n * @category Plugin Development\n * @since 2.13.0\n */\nexport interface ScrollMapping {\n /** Raw row-content height in pixels (totalRows * rowHeight or sum of variable heights). */\n rawContentHeight: number;\n /** Effective spacer DOM height — `min(rawContentHeight, maxSpacerHeight)`. */\n spacerHeight: number;\n /** Viewport height used for computing scrollable extents. */\n viewportHeight: number;\n /** True when `rawContentHeight > maxSpacerHeight` — fractional mapping is active. */\n capped: boolean;\n}\n\n/**\n * Compute a mapping between the spacer's native scrollTop and the virtual\n * row-content scroll offset.\n *\n * For datasets within `maxSpacerHeight`, mapping is identity (no transform).\n * Above the cap, the spacer is clamped and `scrollTop` units no longer equal\n * row-content units — call {@link toVirtualScrollTop} / {@link fromVirtualScrollTop}\n * to translate between the two coordinate spaces.\n *\n * @category Plugin Development\n * @since 2.13.0\n */\nexport function computeScrollMapping(\n rawContentHeight: number,\n viewportHeight: number,\n maxSpacerHeight = MAX_ELEMENT_HEIGHT_PX,\n): ScrollMapping {\n const spacerHeight = Math.min(rawContentHeight, maxSpacerHeight);\n return {\n rawContentHeight,\n spacerHeight,\n viewportHeight,\n capped: rawContentHeight > maxSpacerHeight,\n };\n}\n\n/**\n * Translate a native `scrollTop` (spacer space) into a virtual scroll offset\n * in raw row-content space. Identity when the mapping is not capped.\n *\n * The result is clamped to `[0, rawContentHeight - viewportHeight]` so callers\n * never index past the dataset, even when the spacer's actual scrollable extent\n * slightly exceeds `spacerHeight - viewportHeight` (e.g. when a horizontal scrollbar\n * adds bottom padding to the faux spacer).\n *\n * @category Plugin Development\n * @since 2.13.0\n */\nexport function toVirtualScrollTop(scrollTop: number, mapping: ScrollMapping): number {\n if (!mapping.capped) return scrollTop;\n const sMax = mapping.spacerHeight - mapping.viewportHeight;\n const rMax = mapping.rawContentHeight - mapping.viewportHeight;\n if (sMax <= 0 || rMax <= 0) return scrollTop;\n const virtual = (scrollTop * rMax) / sMax;\n if (virtual < 0) return 0;\n if (virtual > rMax) return rMax;\n return virtual;\n}\n\n/**\n * Translate a virtual scroll offset (raw row-content space) into a native\n * `scrollTop` value (spacer space). Identity when the mapping is not capped.\n *\n * @category Plugin Development\n * @since 2.13.0\n */\nexport function fromVirtualScrollTop(virtualTop: number, mapping: ScrollMapping): number {\n if (!mapping.capped) return virtualTop;\n const sMax = mapping.spacerHeight - mapping.viewportHeight;\n const rMax = mapping.rawContentHeight - mapping.viewportHeight;\n if (sMax <= 0 || rMax <= 0) return virtualTop;\n return (virtualTop * sMax) / rMax;\n}\n\n/**\n * Identity scroll mapping (no cap applied). Useful as a default in places that\n * don't yet know the dataset extents. `viewportHeight` is `0` until the first\n * `calculateTotalSpacerHeight` call populates the real extents — safe because\n * `capped: false` short-circuits both translation helpers before they read it.\n *\n * @category Plugin Development\n * @since 2.13.0\n */\nexport function createIdentityScrollMapping(): ScrollMapping {\n return { rawContentHeight: 0, spacerHeight: 0, viewportHeight: 0, capped: false };\n}\n\n// #endregion\n\n// #region Scroll Transform\n\n/**\n * Sub-pixel `translateY` for the `.rows` element at a given native scroll position.\n *\n * Bypass mode (all rows rendered) tracks the raw scroll offset directly. Virtualized mode\n * offsets only the remainder past the window's first row, and rounds that start down to an\n * even index so DOM `nth-child(even)` zebra parity keeps matching data-row parity.\n *\n * @param v Virtual state (structural subset of `VirtualState`).\n * @param rawScrollTop Native `scrollTop` of the faux scrollbar (clamped spacer space).\n */\nexport function computeRowsTranslateY(\n v: {\n rowHeight: number;\n bypassThreshold: number;\n variableHeights: boolean;\n positionCache: RowPosition[] | null;\n scrollMapping: ScrollMapping;\n },\n totalRows: number,\n rawScrollTop: number,\n): number {\n if (totalRows <= v.bypassThreshold) return -rawScrollTop;\n\n const currentScrollTop = toVirtualScrollTop(rawScrollTop, v.scrollMapping);\n const cache = v.positionCache;\n let startRowOffset: number;\n\n if (v.variableHeights && cache && cache.length > 0) {\n let rawStart = getRowIndexAtOffset(cache, currentScrollTop);\n if (rawStart === -1) rawStart = 0;\n const evenAlignedStart = rawStart - (rawStart % 2);\n startRowOffset = cache[evenAlignedStart]?.offset ?? evenAlignedStart * v.rowHeight;\n } else {\n const rawStart = Math.floor(currentScrollTop / v.rowHeight);\n startRowOffset = (rawStart - (rawStart % 2)) * v.rowHeight;\n }\n\n return -(currentScrollTop - startRowOffset);\n}\n\n// #endregion\n\n// #region Fixed-Height Virtual Window\n\n/**\n * Result of computing a virtual window for fixed-height rows.\n * Used by plugins like FilteringPlugin for virtualized dropdowns.\n */\nexport interface VirtualWindow {\n /** First row index to render (inclusive) */\n start: number;\n /** Last row index to render (exclusive) */\n end: number;\n /** Pixel offset to apply to the rows container (translateY) */\n offsetY: number;\n /**\n * Effective spacer DOM height — use this for the scrollable spacer element.\n * Equal to `min(totalRows * rowHeight, maxSpacerHeight)`. Above the cap this\n * is clamped; use {@link VirtualWindow.rawContentHeight} for the un-clamped value.\n */\n totalHeight: number;\n /**\n * Raw, un-clamped row-content height (`totalRows * rowHeight`). Equal to\n * `totalHeight` when the dataset is below `maxSpacerHeight`.\n *\n * @since 2.13.0\n */\n rawContentHeight: number;\n}\n\n/** Parameters for computing the virtual window */\nexport interface VirtualWindowParams {\n /** Total number of rows */\n totalRows: number;\n /** Height of the viewport in pixels */\n viewportHeight: number;\n /** Current scroll top position */\n scrollTop: number;\n /** Height of each row in pixels */\n rowHeight: number;\n /** Number of extra rows to render above/below viewport */\n overscan: number;\n /**\n * Optional cap on the spacer's DOM height. When `totalRows * rowHeight` exceeds\n * this value, `scrollTop` is treated as a spacer-space position and translated\n * to virtual row-content space via fractional mapping. Defaults to {@link MAX_ELEMENT_HEIGHT_PX}.\n * Pass `Infinity` to disable.\n *\n * @since 2.13.0\n */\n maxSpacerHeight?: number;\n}\n\n/**\n * Compute the virtual row window based on scroll position and viewport.\n * Simple fixed-height implementation for plugins needing basic virtualization.\n *\n * For datasets where `totalRows * rowHeight > maxSpacerHeight`, scroll position\n * is mapped fractionally so the entire dataset stays reachable past the browser's\n * single-element height cap (Chromium ~33.5M px). See {@link computeScrollMapping}.\n *\n * @param params - Parameters for computing the window\n * @returns VirtualWindow with start/end indices and transforms\n */\nexport function computeVirtualWindow(params: VirtualWindowParams): VirtualWindow {\n const { totalRows, viewportHeight, scrollTop, rowHeight, overscan, maxSpacerHeight = MAX_ELEMENT_HEIGHT_PX } = params;\n\n const visibleRows = Math.ceil(viewportHeight / rowHeight);\n const rawContentHeight = totalRows * rowHeight;\n const mapping = computeScrollMapping(rawContentHeight, viewportHeight, maxSpacerHeight);\n const virtualScrollTop = toVirtualScrollTop(scrollTop, mapping);\n\n // Render overscan rows in each direction (total window = visible + 2*overscan)\n let start = Math.floor(virtualScrollTop / rowHeight) - overscan;\n if (start < 0) start = 0;\n\n let end = start + visibleRows + overscan * 2;\n if (end > totalRows) end = totalRows;\n\n // Ensure start is adjusted if we hit the end\n if (end === totalRows && start > 0) {\n start = Math.max(0, end - visibleRows - overscan * 2);\n }\n\n return {\n start,\n end,\n offsetY: start * rowHeight,\n totalHeight: mapping.spacerHeight,\n rawContentHeight,\n };\n}\n\n/**\n * Determine if virtualization should be bypassed for small datasets.\n * When there are very few items, the overhead of virtualization isn't worth it.\n *\n * @param totalRows - Total number of items\n * @param threshold - Bypass threshold (skip virtualization if totalRows <= threshold)\n * @returns True if virtualization should be bypassed\n */\nexport function shouldBypassVirtualization(totalRows: number, threshold: number): boolean {\n return totalRows <= threshold;\n}\n\n// #endregion\n","/**\n * Central keyboard handler attached to the host element. Manages navigation, paging,\n * and edit lifecycle triggers while respecting active form field interactions.\n */\nimport { FOCUSABLE_EDITOR_SELECTOR, GridClasses } from '../constants';\nimport type { GridHost } from '../types';\nimport { clearCellFocus, isRTL } from './utils';\nimport { readCellField } from './value-accessor';\nimport { fromVirtualScrollTop, getRowIndexAtOffset, toVirtualScrollTop } from './virtualization';\n\n/** Commit active row edit if the editing plugin provides this method. */\nfunction tryCommitEdit(grid: GridHost): void {\n if (typeof grid.commitActiveRowEdit === 'function') grid.commitActiveRowEdit();\n}\n\nfunction isFormField(el: HTMLElement | null): boolean {\n if (!el) return false;\n const tag = el.tagName;\n if (tag === 'INPUT' || tag === 'SELECT' || tag === 'TEXTAREA') return true;\n if (el.isContentEditable) return true;\n return false;\n}\n\n/** True while a row-scoped edit session is active. */\nfunction isRowEditing(grid: GridHost): boolean {\n return grid._activeEditRows !== undefined && grid._activeEditRows !== -1;\n}\n\n// #region Keyboard Handler\n/**\n * Guard chain deciding whether this keydown belongs to something other than\n * cell navigation and must be left alone.\n */\nfunction shouldIgnoreKeyDown(grid: GridHost, e: KeyboardEvent, target: HTMLElement | null): boolean {\n // Keyboard events whose target lives inside the grid host but OUTSIDE the\n // rows body (toolpanel inputs/buttons, shell-header controls, light-DOM\n // children of <tbw-grid>) own their own keyboard interactions. Treating\n // ArrowUp/ArrowDown/Enter on such a target as cell navigation steals the\n // user's actual interaction target on the very next render cycle.\n // Cells and the grid host itself remain valid navigation targets.\n if (target && target !== grid && !target.closest?.('.rows-body')) return true;\n\n if (isFormField(target)) {\n switch (e.key) {\n // Home/End and horizontal arrows navigate within the text input; Enter\n // and Escape are handled by the input's own handlers first.\n case 'Home':\n case 'End':\n case 'ArrowLeft':\n case 'ArrowRight':\n case 'Enter':\n case 'Escape':\n return true;\n case 'ArrowUp':\n case 'ArrowDown':\n // Number spinners own the vertical arrows; other fields fall through.\n if ((target as HTMLInputElement).tagName === 'INPUT' && (target as HTMLInputElement).type === 'number') {\n return true;\n }\n break;\n }\n }\n\n const colType = grid._visibleColumns[grid._focusCol]?.type;\n return isRowEditing(grid) && colType === 'select' && (e.key === 'ArrowDown' || e.key === 'ArrowUp');\n}\n\n/** Tab / Shift+Tab: advance one cell, wrapping to the next/previous row. */\nfunction navigateTab(grid: GridHost, e: KeyboardEvent, maxRow: number, maxCol: number): void {\n e.preventDefault();\n if (!e.shiftKey) {\n if (grid._focusCol < maxCol) {\n grid._focusCol += 1;\n return;\n }\n tryCommitEdit(grid);\n if (grid._focusRow < maxRow) {\n grid._focusRow += 1;\n grid._focusCol = 0;\n }\n return;\n }\n if (grid._focusCol > 0) {\n grid._focusCol -= 1;\n } else if (grid._focusRow > 0) {\n if (grid._activeEditRows === grid._focusRow) tryCommitEdit(grid);\n grid._focusRow -= 1;\n grid._focusCol = maxCol;\n }\n}\n\n/**\n * ArrowLeft/ArrowRight. `towardEnd` is the LOGICAL direction (ArrowRight); in\n * RTL the physical arrow maps to the opposite column index.\n */\nfunction navigateHorizontal(grid: GridHost, e: KeyboardEvent, maxCol: number, towardEnd: boolean): void {\n const forward = isRTL(grid) ? !towardEnd : towardEnd;\n grid._focusCol = forward ? Math.min(maxCol, grid._focusCol + 1) : Math.max(0, grid._focusCol - 1);\n e.preventDefault();\n}\n\n/** Commit any active row edit, then focus the first/last row (column unchanged). */\nfunction jumpToRowEdge(grid: GridHost, maxRow: number, toEnd: boolean): void {\n if (isRowEditing(grid)) tryCommitEdit(grid);\n grid._focusRow = toEnd ? maxRow : 0;\n}\n\n/** ArrowUp/ArrowDown: one row, or the first/last row with Ctrl/Cmd. */\nfunction navigateVertical(grid: GridHost, e: KeyboardEvent, maxRow: number, down: boolean): void {\n if (e.ctrlKey || e.metaKey) jumpToRowEdge(grid, maxRow, down);\n else {\n if (isRowEditing(grid)) tryCommitEdit(grid);\n grid._focusRow = down ? Math.min(maxRow, grid._focusRow + 1) : Math.max(0, grid._focusRow - 1);\n }\n e.preventDefault();\n}\n\n/**\n * PageUp/PageDown target: one viewport of rows (less any overlay band), so an\n * unconstrained grid pages straight to its edge. 20 rows until layout exists.\n */\nfunction pageTargetRow(grid: GridHost, maxRow: number, down: boolean): number {\n const from = grid._focusRow;\n const virt = grid._virtualization;\n const viewportH = virt?.viewportEl?.clientHeight ?? virt?.container?.clientHeight ?? 0;\n const offsets = grid._getVerticalScrollOffsets?.(from);\n const usable = viewportH - Math.max(0, offsets?.top ?? 0) - Math.max(0, offsets?.bottom ?? 0);\n const pc = virt?.variableHeights ? virt.positionCache : null;\n let target: number;\n if (usable > 0 && pc?.[from]) {\n target = getRowIndexAtOffset(pc, pc[from].offset + (down ? usable : -usable));\n // A row taller than the viewport would otherwise pin focus in place.\n if (target === from) target = from + (down ? 1 : -1);\n } else {\n const rowH = virt?.rowHeight ?? 0;\n const step = usable > 0 && rowH > 0 ? Math.max(1, Math.floor(usable / rowH)) : 20;\n target = from + (down ? step : -step);\n }\n return Math.max(0, Math.min(maxRow, target));\n}\n\n/** Home/End (plus Ctrl/Cmd variants which also jump to the first/last row). */\nfunction navigateRowEdge(grid: GridHost, e: KeyboardEvent, maxRow: number, maxCol: number, toEnd: boolean): void {\n if (e.ctrlKey || e.metaKey) jumpToRowEdge(grid, maxRow, toEnd);\n grid._focusCol = toEnd ? maxCol : 0;\n e.preventDefault();\n ensureCellVisible(grid, toEnd ? { forceScrollRight: true } : { forceScrollLeft: true });\n}\n\n/** Dispatch the unified `cell-activate` event. Returns true when cancelled. */\nfunction emitCellActivate(grid: GridHost, e: KeyboardEvent): boolean {\n const rowIndex = grid._focusRow;\n const colIndex = grid._focusCol;\n const column = grid._visibleColumns[colIndex];\n const row = grid._rows[rowIndex];\n const field = column?.field ?? '';\n const activateEvent = new CustomEvent('cell-activate', {\n cancelable: true,\n detail: {\n rowIndex,\n colIndex,\n column,\n field,\n value: field && row ? readCellField(row, field) : undefined,\n row,\n cellEl: grid.querySelector(`[data-row=\"${rowIndex}\"][data-col=\"${colIndex}\"]`) as HTMLElement | undefined,\n trigger: 'keyboard' as const,\n originalEvent: e,\n },\n });\n grid.dispatchEvent(activateEvent);\n return activateEvent.defaultPrevented;\n}\n\nexport function handleGridKeyDown(grid: GridHost, e: KeyboardEvent): void {\n // Dispatch to plugin system first - if any plugin handles it, stop here\n if (grid._dispatchKeyDown?.(e)) return;\n\n const path = e.composedPath?.() ?? [];\n const target = (path.length ? path[0] : e.target) as HTMLElement | null;\n if (shouldIgnoreKeyDown(grid, e, target)) return;\n\n const maxRow = grid._rows.length - 1;\n const maxCol = grid._visibleColumns.length - 1;\n\n switch (e.key) {\n case 'Tab':\n navigateTab(grid, e, maxRow, maxCol);\n break;\n case 'ArrowDown':\n navigateVertical(grid, e, maxRow, true);\n break;\n case 'ArrowUp':\n navigateVertical(grid, e, maxRow, false);\n break;\n case 'ArrowRight':\n navigateHorizontal(grid, e, maxCol, true);\n break;\n case 'ArrowLeft':\n navigateHorizontal(grid, e, maxCol, false);\n break;\n case 'Home':\n navigateRowEdge(grid, e, maxRow, maxCol, false);\n return;\n case 'End':\n navigateRowEdge(grid, e, maxRow, maxCol, true);\n return;\n case 'PageDown':\n grid._focusRow = pageTargetRow(grid, maxRow, true);\n e.preventDefault();\n break;\n case 'PageUp':\n grid._focusRow = pageTargetRow(grid, maxRow, false);\n e.preventDefault();\n break;\n // NOTE: Enter is normally handled by EditingPlugin. If no plugin handled\n // it, dispatch the unified cell-activate event for custom handling.\n case 'Enter':\n if (emitCellActivate(grid, e)) {\n e.preventDefault();\n return;\n }\n break;\n default:\n return;\n }\n ensureCellVisible(grid);\n}\n// #endregion\n\n// #region Cell Visibility\n/**\n * Options for ensureCellVisible to control scroll behavior.\n */\ninterface EnsureCellVisibleOptions {\n /** Force scroll to the leftmost position (for Home key) */\n forceScrollLeft?: boolean;\n /** Force scroll to the rightmost position (for End key) */\n forceScrollRight?: boolean;\n /** Force horizontal scroll even in edit mode (for Tab navigation) */\n forceHorizontalScroll?: boolean;\n}\n\n/**\n * Vertical: scroll the virtualized container so the focused row is in view.\n *\n * `y` is in raw row-content space (rowHeight × index). Above the browser height\n * cap (#326), `scrollEl.scrollTop` is in clamped spacer space — the two\n * coordinate systems are not interchangeable, so translate scrollTop into\n * virtual space for the comparison and back into native space when writing.\n * For sub-cap datasets the mapping is identity, so this is a no-op.\n *\n * Plugins that paint over the viewport (sticky rows) report the space they\n * obscure via `_getVerticalScrollOffsets`, so the row lands clear of the\n * overlay instead of underneath it — WCAG 2.2 SC 2.4.11 Focus Not Obscured.\n */\nfunction scrollFocusedRowIntoView(grid: GridHost): void {\n if (!grid._virtualization?.enabled) return;\n const { rowHeight, container, viewportEl, scrollMapping } = grid._virtualization;\n // container is the faux scrollbar element that handles actual scrolling;\n // viewportEl is the visible area element that has the correct height.\n const scrollEl = container as HTMLElement | undefined;\n const visibleHeight = viewportEl?.clientHeight ?? scrollEl?.clientHeight ?? 0;\n if (!scrollEl || visibleHeight <= 0) return;\n\n const offsets = grid._getVerticalScrollOffsets?.(grid._focusRow) ?? { top: 0, bottom: 0 };\n if (offsets.skipScroll) return;\n // A malformed or oversized report must never invert the visible band.\n const top = Math.max(0, offsets.top);\n const bottom = Math.max(0, offsets.bottom);\n const usableHeight = visibleHeight - top - bottom;\n if (usableHeight <= 0) return;\n\n const y = grid._focusRow * rowHeight;\n const virtualScrollTop = scrollMapping ? toVirtualScrollTop(scrollEl.scrollTop, scrollMapping) : scrollEl.scrollTop;\n if (y < virtualScrollTop + top) {\n const target = Math.max(0, y - top);\n scrollEl.scrollTop = scrollMapping ? fromVirtualScrollTop(target, scrollMapping) : target;\n } else if (y + rowHeight > virtualScrollTop + top + usableHeight) {\n const target = Math.max(0, y - usableHeight + rowHeight - top);\n scrollEl.scrollTop = scrollMapping ? fromVirtualScrollTop(target, scrollMapping) : target;\n }\n}\n\n/**\n * Resolve the rendered cell for the focused row/col, or `undefined` when the\n * focused row is outside the current virtual window.\n */\nfunction findFocusedCell(grid: GridHost): { rowEl: HTMLElement | null; cell: HTMLElement } | undefined {\n const rowIndex = grid._focusRow;\n const vStart = grid._virtualization.start ?? 0;\n const vEnd = grid._virtualization.end ?? grid._rows.length;\n if (rowIndex < vStart || rowIndex >= vEnd) return undefined;\n\n const rowEl = grid._bodyEl.querySelectorAll('.data-grid-row')[rowIndex - vStart] as HTMLElement | null;\n // Try exact column match first, then query by data-col, then fall back to the\n // first cell (full-width group rows).\n let cell = rowEl?.children[grid._focusCol] as HTMLElement | undefined;\n if (!cell || !cell.classList?.contains('cell')) {\n cell = (rowEl?.querySelector(`.cell[data-col=\"${grid._focusCol}\"]`) ?? rowEl?.querySelector('.cell[data-col]')) as\n HTMLElement | undefined;\n }\n return cell ? { rowEl: rowEl ?? null, cell } : undefined;\n}\n\n/** Horizontal: bring the focused cell into the `.tbw-scroll-area` viewport. */\nfunction scrollCellIntoViewHorizontally(\n grid: GridHost,\n rowEl: HTMLElement | null,\n cell: HTMLElement,\n options?: EnsureCellVisibleOptions,\n): void {\n const scrollArea = grid.querySelector('.tbw-scroll-area') as HTMLElement | null;\n if (!scrollArea) return;\n\n // Home/End always scroll to the edge.\n if (options?.forceScrollLeft) {\n scrollArea.scrollLeft = 0;\n return;\n }\n if (options?.forceScrollRight) {\n scrollArea.scrollLeft = scrollArea.scrollWidth - scrollArea.clientWidth;\n return;\n }\n\n // Plugins (e.g. pinned columns) report how much of the scroll area they\n // obscure, and whether the focused cell should skip scrolling entirely\n // (pinned cells are always visible).\n const offsets = grid._getHorizontalScrollOffsets?.(rowEl ?? undefined, cell) ?? { left: 0, right: 0 };\n if (offsets.skipScroll) return;\n\n const cellRect = cell.getBoundingClientRect();\n const scrollAreaRect = scrollArea.getBoundingClientRect();\n const cellLeft = cellRect.left - scrollAreaRect.left + scrollArea.scrollLeft;\n const cellRight = cellLeft + cellRect.width;\n const visibleLeft = scrollArea.scrollLeft + offsets.left;\n const visibleRight = scrollArea.scrollLeft + scrollArea.clientWidth - offsets.right;\n if (cellLeft < visibleLeft) {\n scrollArea.scrollLeft = cellLeft - offsets.left;\n } else if (cellRight > visibleRight) {\n scrollArea.scrollLeft = cellRight - scrollArea.clientWidth + offsets.right;\n }\n}\n\n/** Move real DOM focus to the editor, the cell, or the grid host. */\nfunction applyFocusTarget(grid: GridHost, cell: HTMLElement, isEditing: boolean): void {\n if (isEditing && cell.classList.contains(GridClasses.EDITING)) {\n // Editing cell: focus the editor input inside it\n const focusTarget = cell.querySelector(FOCUSABLE_EDITOR_SELECTOR) as HTMLElement | null;\n if (focusTarget && document.activeElement !== focusTarget) {\n try {\n focusTarget.focus({ preventScroll: true });\n } catch {\n /* empty */\n }\n }\n return;\n }\n\n if (isEditing) {\n // Active edit row but this cell isn't the editing cell — focus it so Tab\n // navigation within the row can attach editors.\n if (cell.contains(document.activeElement)) return;\n if (!cell.hasAttribute('tabindex')) cell.setAttribute('tabindex', '-1');\n try {\n cell.focus({ preventScroll: true });\n } catch {\n /* empty */\n }\n return;\n }\n\n // NOT editing: keep focus on the grid element (tabindex=0) rather than\n // individual cells. In a virtualized grid, cells can be detached by\n // subsequent render cycles (e.g., SelectionPlugin's requestAfterRender\n // → RAF → row recycling). A detached focused cell causes activeElement\n // to revert to <body>, breaking keyboard navigation.\n // Visual focus is managed by the .cell-focus CSS class + data-has-focus.\n //\n // BUT: don't steal focus from a real focused descendant (toolpanel\n // input, shell-header button, registered overlay control). Cell focus\n // is virtual; if the user is actively typing in a non-cell input, we\n // must leave that input alone. We only reclaim focus when nothing\n // meaningful inside the grid currently holds it (active element is\n // outside grid, on the grid host itself, or on a bare cell).\n const active = document.activeElement;\n const meaningful =\n active instanceof HTMLElement &&\n active !== grid &&\n typeof grid.contains === 'function' &&\n grid.contains(active) &&\n !active.classList.contains('cell') &&\n !active.closest('.cell');\n if (!meaningful && active !== grid) {\n grid.focus({ preventScroll: true });\n }\n}\n\n/**\n * Scroll the viewport (virtualized or static) so the focused cell's row is visible\n * and apply visual focus styling / tabindex management.\n */\nexport function ensureCellVisible(grid: GridHost, options?: EnsureCellVisibleOptions): void {\n scrollFocusedRowIntoView(grid);\n\n const isEditing = isRowEditing(grid) || !!grid._isGridEditMode;\n // Skip refreshVirtualWindow when in edit mode to avoid wiping editors\n if (!isEditing) grid.refreshVirtualWindow(false);\n\n clearCellFocus(grid._bodyEl);\n // Clear previous aria-selected markers\n Array.from(grid._bodyEl.querySelectorAll('[aria-selected=\"true\"]')).forEach((el) => {\n el.setAttribute('aria-selected', 'false');\n });\n\n const focused = findFocusedCell(grid);\n if (!focused) return;\n const { rowEl, cell } = focused;\n\n cell.classList.add('cell-focus');\n cell.setAttribute('aria-selected', 'true');\n\n // Skip horizontal scrolling in edit mode to prevent scroll jumps when editors\n // are created — unless forceHorizontalScroll is set (Tab while editing).\n if (!isEditing || options?.forceHorizontalScroll) {\n scrollCellIntoViewHorizontally(grid, rowEl, cell, options);\n }\n\n applyFocusTarget(grid, cell, isEditing);\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, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''');\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 * 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 * 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 * Responsive Plugin CSS\n *\n * Card layout styles for narrow viewports.\n * Applied when [data-responsive] attribute is present on the grid.\n *\n * IMPORTANT: This CSS intentionally preserves the grid's virtualization system.\n * The row height observer will detect the new card height automatically and\n * recalculate the virtual window. Do NOT break virtualization by:\n * - Changing .rows-viewport overflow\n * - Changing .rows position\n * - Hiding .faux-vscroll\n */\n\n/* ============================================\n Animation / Transition Support\n ============================================ */\n\n/* View transition path: the compositor cross-fades the whole grid (or, with\n `animation: 'morph-rows' | 'morph-cells'`, each row or cell) between the table\n and card layouts. While this is active the plugin withholds\n `data-responsive-animate`, so the keyframe fade below never runs alongside it. */\ntbw-grid[data-responsive-transition]::view-transition-group(*) {\n animation-duration: var(--tbw-responsive-duration, 200ms);\n}\n\n/* `morph-cells` names every cell individually. One `view-transition-class` keeps\n that a single style rule instead of one rule per generated name — the class\n does NOT capture anything on its own, the plugin still authors the names.\n Cells travel a real distance between the two layouts, so they get an\n ease-in-out curve rather than the root's cross-fade. */\ntbw-grid[data-responsive-transition] .data-grid-row > .cell {\n view-transition-class: tbw-responsive-cell;\n}\n\ntbw-grid[data-responsive-transition]::view-transition-group(.tbw-responsive-cell) {\n animation-timing-function: ease-in-out;\n}\n\n/* A cell's box grows from one column to a full card row, and the UA stretches\n both snapshots to fill that box — so the text visibly zooms while it travels.\n Drawing them at natural size keeps the glyphs the size they actually are at\n both ends; the group still animates the box, so the cell still moves.\n Anchoring right lands the value on the card's right-aligned value slot rather\n than on the `Header:` label, and lets the label reveal as the box widens. */\ntbw-grid[data-responsive-transition]::view-transition-old(.tbw-responsive-cell),\ntbw-grid[data-responsive-transition]::view-transition-new(.tbw-responsive-cell) {\n width: 100%;\n height: 100%;\n object-fit: none;\n object-position: right center;\n}\n\n/* The old bitmap holds its value left-aligned in a column, the new one holds it\n right-aligned in a card — so the two can never line up exactly, and the gap\n varies with the text length. Front-load the cross-fade instead: the swap is\n over while the boxes are still visibly apart, and the rest of the travel runs\n on a single crisp copy. */\ntbw-grid[data-responsive-transition]::view-transition-old(.tbw-responsive-cell),\ntbw-grid[data-responsive-transition]::view-transition-new(.tbw-responsive-cell) {\n animation-duration: calc(var(--tbw-responsive-duration, 200ms) * 0.35);\n}\n\n/* Base transition setup for animated mode switching */\ntbw-grid[data-responsive-animate] {\n .data-grid-row,\n .data-grid-row > .cell {\n transition:\n opacity var(--tbw-responsive-duration, 200ms) ease-out,\n transform var(--tbw-responsive-duration, 200ms) ease-out;\n }\n}\n\n/* Fade-in when entering responsive mode */\ntbw-grid[data-responsive][data-responsive-animate] {\n .data-grid-row {\n animation: responsive-card-enter var(--tbw-responsive-duration, 200ms) ease-out;\n }\n}\n\n@keyframes responsive-card-enter {\n from {\n opacity: 0;\n transform: translateY(-10px);\n }\n to {\n opacity: 1;\n transform: translateY(0);\n }\n}\n\n/* ============================================\n Responsive Card Layout\n ============================================ */\n\ntbw-grid[data-responsive] {\n /* Hide entire header section in card mode */\n .header {\n display: none !important;\n }\n\n /* Hide footer row in card mode (aggregations don't make sense in cards) */\n .footer-row {\n display: none;\n }\n\n /* Override horizontal scroll - single column flow */\n .tbw-scroll-area {\n overflow-x: hidden;\n min-width: 0 !important;\n }\n\n /* Override rows-body-wrapper to allow natural flow */\n .rows-body-wrapper {\n min-width: 0 !important;\n }\n\n /* Convert row from CSS Grid to block layout */\n .data-grid-row:not(.group-row) {\n display: block !important;\n grid-template-columns: none !important;\n padding: var(--tbw-cell-padding);\n padding-inline-start: var(--tbw-spacing-xl);\n border-bottom: 1px solid var(--tbw-color-border);\n /* Disable containment so row can grow to fit stacked cells */\n contain: none !important;\n content-visibility: visible !important;\n background: var(--tbw-color-bg);\n position: relative;\n\n &:nth-child(even) {\n background: var(--tbw-color-row-alt);\n }\n\n &:hover {\n background: var(--tbw-color-row-hover);\n }\n\n &[aria-selected='true'] {\n background: var(--tbw-color-selection);\n\n /* Selection indicator bar */\n &::before {\n content: '';\n position: absolute;\n inset-inline-start: 0;\n top: 0;\n bottom: 0;\n width: 4px;\n background: var(--tbw-color-accent);\n }\n }\n }\n\n /* The default stacked-cell card grows to fit its cells. A custom `cardRenderer`\n row sets its own height from `cardRowHeight`, so it must not be forced open. */\n .data-grid-row:not(.group-row, .responsive-card) {\n min-height: auto !important;\n height: auto !important;\n }\n\n /* Stack cells vertically as label: value pairs */\n .data-grid-row:not(.group-row) > .cell {\n display: flex !important;\n justify-content: space-between;\n align-items: center;\n padding: var(--tbw-spacing-xs) var(--tbw-spacing-md);\n width: 100% !important;\n min-width: 0 !important;\n min-height: auto !important;\n height: auto !important;\n line-height: 1.5 !important;\n position: static !important;\n left: auto !important;\n right: auto !important;\n border: none !important;\n border-bottom: none !important;\n border-inline-end: none !important;\n background: transparent !important;\n white-space: normal !important;\n overflow: visible !important;\n\n /* Header label from data-header attribute */\n &::before {\n content: attr(data-header) ': ';\n font-weight: 600;\n color: var(--tbw-color-header-fg);\n flex-shrink: 0;\n margin-inline-end: var(--tbw-spacing-md);\n min-width: 100px;\n }\n\n /* Value takes remaining space */\n &::after {\n content: none;\n }\n }\n\n /* Hide utility columns (row expander, selection checkbox, etc.) */\n .cell[data-utility] {\n display: none !important;\n }\n}\n\n/* Hidden columns — specificity must beat `.data-grid-row:not(.group-row) > .cell` above */\ntbw-grid[data-responsive] .data-grid-row > .cell[data-responsive-hidden] {\n display: none !important;\n}\n\n/* Value-only columns in responsive mode (hides label, shows value full-width) */\ntbw-grid[data-responsive] .data-grid-row > .cell[data-responsive-value-only] {\n /* Override the header label ::before to hide it */\n &::before {\n display: none !important;\n }\n\n /* Value takes full width, no justify-content needed */\n justify-content: flex-start !important;\n font-weight: 500;\n}\n\n/* Opt-in: hide per-card field labels (the \"Name:\" prefix) when\n `hideHeader: true` is configured. Toggled by ResponsivePlugin. */\ntbw-grid[data-responsive][data-responsive-hide-header] .data-grid-row:not(.group-row) > .cell::before {\n display: none !important;\n}\n\n/* Non-responsive mode: a hidden column collapses its grid track to zero width\n rather than leaving layout. The cell must stay in flow — cells are auto-placed,\n so dropping one would pull every later cell into the wrong track. Padding and\n border are zeroed too, or the cell would floor at ~17px and the track could\n never reach zero. Opacity is deliberately not `!important`: the plugin fades\n the changed cells with WAAPI, and `!important` outranks animations. */\ntbw-grid:not([data-responsive]) .cell[data-responsive-hidden] {\n padding-inline: 0 !important;\n border-inline-width: 0 !important;\n overflow: hidden !important;\n pointer-events: none;\n opacity: 0;\n}\n\n/* `grid-template-columns` is animatable, and a row element survives a render\n (only its cells are rebuilt), so the collapse is a plain transition. Gated on\n the fade window so column resizing stays instant. A track list only\n interpolates pairwise, which holds here — collapsing never changes the number\n of tracks. */\ntbw-grid[data-responsive-column-fade] :is(.header-row, .data-grid-row) {\n transition: grid-template-columns var(--tbw-responsive-duration, 200ms) ease-out;\n}\n\n/* Hide pinned/aggregation rows in responsive mode - they don't make sense in card layout */\ntbw-grid[data-responsive] {\n .tbw-footer,\n .tbw-header-pinned,\n .tbw-pinned-rows,\n .tbw-aggregation-rows {\n display: none !important;\n }\n\n /* Reset min-widths that hold horizontal scroll width */\n .tbw-pinned-rows,\n .tbw-aggregation-rows,\n .tbw-aggregation-row {\n min-width: 0 !important;\n }\n}\n\n/* Custom card renderer styling */\ntbw-grid[data-responsive] .data-grid-row.responsive-card {\n /* Override default cell layout - custom content takes full width */\n display: block !important;\n padding: var(--tbw-cell-padding);\n border-bottom: 1px solid var(--tbw-color-border);\n\n /* Card content should not have cell-based constraints */\n > * {\n width: 100%;\n }\n\n /* Remove default cell ::before headers - custom renderer handles layout */\n .cell::before {\n display: none;\n }\n}\n","/**\n * Responsive Plugin\n *\n * Transforms the grid from tabular layout to a card/list layout when the grid\n * width falls below a configurable breakpoint. This enables grids to work in\n * narrow containers (split-pane UIs, mobile viewports, dashboard widgets).\n *\n * ## Installation\n *\n * ```ts\n * import { ResponsivePlugin } from '@toolbox-web/grid/plugins/responsive';\n *\n * const config: GridConfig = {\n * plugins: [new ResponsivePlugin({ breakpoint: 500 })],\n * };\n * ```\n *\n * ## How It Works\n *\n * 1. ResizeObserver monitors the grid element's width\n * 2. When `width < breakpoint`, adds `data-responsive` attribute to grid\n * 3. CSS transforms cells from horizontal to vertical layout\n * 4. Each cell displays \"Header: Value\" using CSS `::before` pseudo-element\n *\n * @see [Responsive Demo](?path=/story/grid-plugins-responsive--default)\n */\n\nimport { MISSING_BREAKPOINT } from '../../core/internal/diagnostics';\nimport { ensureCellVisible } from '../../core/internal/keyboard';\nimport { evalTemplateString, setSanitizedHTML } from '../../core/internal/sanitize';\nimport { BaseGridPlugin, type GridElement, type PluginManifest, type PluginQuery } from '../../core/plugin/base-plugin';\nimport type { GridHost } from '../../core/types';\nimport styles from './responsive.css?inline';\nimport type {\n BreakpointConfig,\n HiddenColumnConfig,\n ResponsiveAnimation,\n ResponsiveChangeDetail,\n ResponsivePluginConfig,\n} from './types';\n\n/**\n * The subset of the View Transition API this plugin depends on.\n *\n * `Element.startViewTransition()` is not yet in the TypeScript DOM lib, so the\n * shape is declared locally rather than widening the global `Element`.\n */\ntype ViewTransitionStarter = (update: () => Promise<void>) => {\n readonly finished: Promise<void>;\n /** Rejects when the UA skips the transition — routine while dragging a resize. */\n readonly ready?: Promise<void>;\n};\ntype ViewTransitionCapableElement = HTMLElement & { startViewTransition?: ViewTransitionStarter };\n\n/** How finely the layout switch is broken up for the compositor. */\ntype MorphGranularity = 'cells' | 'rows' | 'none';\n\n/** Disambiguates row morph names when several grids transition at once. */\nlet morphInstanceCounter = 0;\n\n/**\n * Above this, per-cell morphing costs more in compositor layers than it buys.\n * Each named cell is 3 animations and 2 snapshot textures, and rows are\n * virtualized — so this bounds the switch by viewport size, not by dataset size.\n */\nconst MAX_MORPH_CELLS = 150;\n\n/** Stands in for a column set that is not currently applied, without allocating. */\nconst EMPTY_FIELD_SET: ReadonlySet<string> = new Set<string>();\n\n/** Tags the column fade so a re-render can cancel its own leftovers and nothing else. */\nconst COLUMN_FADE_ID = 'tbw-responsive-column-fade';\n\n/** Present only while columns are collapsing, so column resizing stays instant. */\nconst COLUMN_FADE_ATTR = 'data-responsive-column-fade';\n\n/**\n * Cancel this plugin's fades on `cell` and nothing else. Cancelling REJECTS\n * `Animation.finished` with an `AbortError`, and nothing awaits it, so without a\n * sink it surfaces as an unhandled rejection.\n */\nfunction cancelColumnFades(cell: HTMLElement): void {\n for (const animation of cell.getAnimations?.() ?? []) {\n if (animation.id !== COLUMN_FADE_ID) continue;\n animation.finished?.catch(() => undefined);\n animation.cancel();\n }\n}\n\n/** Split a grid track list on top-level spaces, keeping `minmax(a, b)` intact. */\nfunction splitTrackList(template: string): string[] {\n const tracks: string[] = [];\n let depth = 0;\n let start = 0;\n for (let i = 0; i < template.length; i++) {\n const char = template[i];\n if (char === '(') depth++;\n else if (char === ')') depth--;\n else if (char === ' ' && depth === 0) {\n if (i > start) tracks.push(template.slice(start, i));\n start = i + 1;\n }\n }\n if (start < template.length) tracks.push(template.slice(start));\n return tracks;\n}\n\n/**\n * The zero-width form of `track`. A track list only interpolates pairwise, so the\n * collapsed track has to keep the same unit family — `1fr` has to become `0fr`,\n * not `0px`. Keywords (`auto`, `max-content`) have no interpolable zero and snap.\n */\nfunction collapsedTrack(track: string): string {\n if (track.startsWith('minmax(')) return track.includes('fr') ? 'minmax(0px, 0fr)' : 'minmax(0px, 0px)';\n if (track.endsWith('fr')) return '0fr';\n if (track.endsWith('%')) return '0%';\n return '0px';\n}\n\n/**\n * Responsive Plugin for tbw-grid\n *\n * Adds automatic card layout mode when the grid width falls below a configurable\n * breakpoint. Perfect for responsive designs, split-pane UIs, and mobile viewports.\n *\n * @template T The row data type\n *\n * @example\n * ```ts\n * // Basic usage - switch to card layout below 500px\n * const config: GridConfig = {\n * plugins: [new ResponsivePlugin({ breakpoint: 500 })],\n * };\n * ```\n *\n * @example\n * ```ts\n * // Hide less important columns in card mode\n * const config: GridConfig = {\n * plugins: [\n * new ResponsivePlugin({\n * breakpoint: 600,\n * hiddenColumns: ['createdAt', 'updatedAt'],\n * }),\n * ],\n * };\n * ```\n *\n * @example\n * ```ts\n * // Custom card renderer for advanced layouts\n * const config: GridConfig = {\n * plugins: [\n * new ResponsivePlugin({\n * breakpoint: 400,\n * cardRenderer: (row) => {\n * const card = document.createElement('div');\n * card.className = 'custom-card';\n * card.innerHTML = `<strong>${row.name}</strong><br>${row.email}`;\n * return card;\n * },\n * }),\n * ],\n * };\n * ```\n * @since 1.1.0\n */\nexport class ResponsivePlugin<T = unknown> extends BaseGridPlugin<ResponsivePluginConfig<T>> {\n readonly name = 'responsive';\n override readonly version = '1.0.0';\n override readonly styles = styles;\n\n /**\n * Plugin manifest declaring queries this plugin handles.\n */\n static override readonly manifest: PluginManifest = {\n queries: [\n {\n type: 'isCardMode',\n description: 'Returns whether the grid is currently in responsive card mode',\n },\n ],\n };\n\n #resizeObserver?: ResizeObserver;\n #isResponsive = false;\n #debounceTimer?: ReturnType<typeof setTimeout>;\n #warnedAboutMissingBreakpoint = false;\n #currentWidth = 0;\n /** Width the last `#checkBreakpoint()` ran against — guards height-only resizes. */\n #lastCheckedWidth?: number;\n /** Timestamp of the last applied layout switch, for the post-switch cooldown. */\n #lastSwitchAt = Number.NEGATIVE_INFINITY;\n /** Unique `view-transition-name` prefixes for this instance's rows and cells. */\n readonly #morphInstance = ++morphInstanceCounter;\n readonly #morphPrefix = `tbw-responsive-row-${this.#morphInstance}-`;\n readonly #cellMorphPrefix = `tbw-responsive-cell-${this.#morphInstance}-`;\n /** Identifies the newest view transition, so superseded ones skip their cleanup. */\n #transitionToken = 0;\n /** Set of column fields to completely hide */\n #hiddenColumnSet: Set<string> = new Set();\n /** The subset of the above that the last render actually hid. */\n #appliedHiddenFields: ReadonlySet<string> = EMPTY_FIELD_SET;\n /** Set of column fields to show value only (no header label) */\n #valueOnlyColumnSet: Set<string> = new Set();\n /** The subset of the above that the last render actually stripped labels from. */\n #appliedValueOnlyFields: ReadonlySet<string> = EMPTY_FIELD_SET;\n /** Fields currently fading out of the table, and the ones fading in. */\n #fadeOutFields: Set<string> = new Set();\n #fadeInFields: Set<string> = new Set();\n /** When the in-flight column fade ends, so a mid-fade re-render can resume it. */\n #fadeEndsAt = 0;\n #fadeTimer?: ReturnType<typeof setTimeout>;\n /** Whether the grid template currently carries this plugin's collapsed tracks. */\n #templateOverridden = false;\n /** Currently active breakpoint, or null if none */\n #activeBreakpoint: BreakpointConfig | null = null;\n /** Sorted breakpoints from largest to smallest */\n #sortedBreakpoints: BreakpointConfig[] = [];\n\n /** Typed internal grid accessor — centralizes the single required cast. */\n get #internalGrid(): GridHost {\n return this.grid;\n }\n\n /**\n * Check if currently in responsive mode.\n * @returns `true` if the grid is in card layout mode\n */\n isResponsive(): boolean {\n return this.#isResponsive;\n }\n\n /**\n * Force responsive mode regardless of width.\n * Useful for testing or manual control.\n * @param enabled - Whether to enable responsive mode\n */\n setResponsive(enabled: boolean): void {\n if (enabled !== this.#isResponsive) {\n this.#isResponsive = enabled;\n this.#commitLayoutSwitch();\n this.emit('responsive-change', {\n isResponsive: enabled,\n width: this.#currentWidth,\n breakpoint: this.config.breakpoint ?? 0,\n } satisfies ResponsiveChangeDetail);\n }\n }\n\n /**\n * Update breakpoint dynamically.\n * @param width - New breakpoint width in pixels\n */\n setBreakpoint(width: number): void {\n this.config.breakpoint = width;\n // The guard keys off width alone, so a config change must invalidate it.\n this.#lastCheckedWidth = undefined;\n this.#checkBreakpoint(this.#currentWidth);\n }\n\n /**\n * Set a custom card renderer.\n * This allows framework adapters to provide template-based renderers at runtime.\n * @param renderer - The card renderer function, or undefined to use default\n */\n setCardRenderer(renderer: ResponsivePluginConfig<T>['cardRenderer']): void {\n this.config.cardRenderer = renderer;\n // If already in responsive mode, trigger a re-render to apply the new renderer\n if (this.#isResponsive) {\n this.requestRender();\n }\n }\n\n /**\n * Get current grid width.\n * @returns Width of the grid element in pixels\n */\n getWidth(): number {\n return this.#currentWidth;\n }\n\n /**\n * Get the currently active breakpoint config (multi-breakpoint mode only).\n * @returns The active BreakpointConfig, or null if no breakpoint is active\n */\n getActiveBreakpoint(): BreakpointConfig | null {\n return this.#activeBreakpoint;\n }\n\n override attach(grid: GridElement): void {\n super.attach(grid);\n\n // Parse light DOM configuration first (may update this.config)\n this.#parseLightDomCard();\n\n // Build hidden column sets from config\n this.#buildHiddenColumnSets(this.config.hiddenColumns);\n\n // Sort breakpoints from largest to smallest for evaluation\n if (this.config.breakpoints?.length) {\n this.#sortedBreakpoints = [...this.config.breakpoints].sort((a, b) => b.maxWidth - a.maxWidth);\n }\n\n this.#syncMotionDuration();\n\n // A `gridConfig` rebuild detaches and re-attaches this same instance, which\n // strips the attributes but keeps the layout state — so re-assert them here or\n // the grid sits in card mode with none of the card CSS until the next resize.\n this.#syncLayoutAttributes();\n\n // Card height is measured from the DOM and outranks the config, so a rebuild\n // carrying a new `cardRowHeight` would keep serving the previous measurement.\n this.#measuredCardHeight = undefined;\n this.#measuredGroupRowHeight = undefined;\n\n // The cards on screen were rendered under the previous config.\n if (this.#isResponsive) this.requestRender();\n\n // Observe the grid element itself (not internal viewport)\n // This captures the container width including when shell panels open/close\n this.#resizeObserver = new ResizeObserver((entries) => {\n this.#onResize(entries[0]?.contentRect.width ?? 0);\n });\n\n this.#resizeObserver.observe(this.gridElement);\n }\n\n /**\n * React to an observed size change.\n *\n * ResizeObserver also fires for HEIGHT changes, and host apps commonly animate\n * a container's height (or load content into it) while the width is already\n * final. A trailing-only debounce is reset by every one of those, delaying the\n * layout switch long past the moment the final width was known. So:\n *\n * 1. Height-only notifications are dropped — they cannot change the outcome.\n * 2. A width change switches on the LEADING edge when no switch happened in\n * the last `debounceMs`, so a settled width applies within one frame.\n * 3. Further width changes inside that window are deferred to a single\n * trailing evaluation, which rate-limits thrash during a resize drag.\n */\n #onResize(width: number): void {\n this.#currentWidth = width;\n\n // Height-only resize — nothing the breakpoint logic reads has changed.\n if (width === this.#lastCheckedWidth) return;\n\n const debounceMs = this.config.debounceMs ?? 100;\n const sinceLastSwitch = performance.now() - this.#lastSwitchAt;\n\n clearTimeout(this.#debounceTimer);\n\n if (sinceLastSwitch >= debounceMs) {\n this.#lastCheckedWidth = width;\n this.#checkBreakpoint(width);\n return;\n }\n\n this.#debounceTimer = setTimeout(() => {\n this.#lastCheckedWidth = this.#currentWidth;\n this.#checkBreakpoint(this.#currentWidth);\n }, debounceMs - sinceLastSwitch);\n }\n\n // #region Light DOM Parsing\n\n /**\n * Parse `<tbw-grid-responsive-card>` elements from the grid's light DOM.\n *\n * Allows declarative configuration:\n * ```html\n * <tbw-grid [rows]=\"data\">\n * <tbw-grid-responsive-card breakpoint=\"500\" card-row-height=\"80\">\n * <div class=\"custom-card\">\n * <strong>{{ row.name }}</strong>\n * <span>{{ row.email }}</span>\n * </div>\n * </tbw-grid-responsive-card>\n * </tbw-grid>\n * ```\n *\n * Attributes:\n * - `breakpoint`: number - Width threshold for responsive mode\n * - `card-row-height`: number | 'auto' - Card height (default: 'auto')\n * - `hidden-columns`: string - Comma-separated fields to hide\n * - `hide-header`: 'true' | 'false' - Hide per-card field labels (default: 'false')\n * - `debounce-ms`: number - Resize debounce delay (default: 100)\n */\n #parseLightDomCard(): void {\n const gridEl = this.gridElement;\n if (!gridEl) return;\n\n const cardEl = gridEl.querySelector('tbw-grid-responsive-card');\n if (!cardEl) return;\n\n // Check if a framework adapter wants to handle this element\n // (e.g., React adapter intercepts for JSX rendering)\n const adapter = this.#internalGrid.__frameworkAdapter;\n if (adapter?.parseResponsiveCardElement) {\n const adapterRenderer = adapter.parseResponsiveCardElement(cardEl);\n if (adapterRenderer) {\n this.config = { ...this.config, cardRenderer: adapterRenderer };\n // Continue to parse attributes even if adapter provides renderer\n }\n }\n\n // Parse attributes for configuration\n const breakpointAttr = cardEl.getAttribute('breakpoint');\n const cardRowHeightAttr = cardEl.getAttribute('card-row-height');\n const hiddenColumnsAttr = cardEl.getAttribute('hidden-columns');\n const hideHeaderAttr = cardEl.getAttribute('hide-header');\n const debounceMsAttr = cardEl.getAttribute('debounce-ms');\n\n const configUpdates: Partial<ResponsivePluginConfig<T>> = {};\n\n if (breakpointAttr !== null) {\n const breakpoint = parseInt(breakpointAttr, 10);\n if (!isNaN(breakpoint)) {\n configUpdates.breakpoint = breakpoint;\n }\n }\n\n if (cardRowHeightAttr !== null) {\n configUpdates.cardRowHeight = cardRowHeightAttr === 'auto' ? 'auto' : parseInt(cardRowHeightAttr, 10);\n }\n\n if (hiddenColumnsAttr !== null) {\n // Parse comma-separated field names\n configUpdates.hiddenColumns = hiddenColumnsAttr\n .split(',')\n .map((s) => s.trim())\n .filter((s) => s.length > 0);\n }\n\n if (hideHeaderAttr !== null) {\n configUpdates.hideHeader = hideHeaderAttr !== 'false';\n }\n\n if (debounceMsAttr !== null) {\n const debounceMs = parseInt(debounceMsAttr, 10);\n if (!isNaN(debounceMs)) {\n configUpdates.debounceMs = debounceMs;\n }\n }\n\n // Get template content from innerHTML (only if no renderer already set)\n const templateHTML = cardEl.innerHTML.trim();\n if (templateHTML && !this.config.cardRenderer && !adapter?.parseResponsiveCardElement) {\n // Create a template-based renderer using the inner HTML\n configUpdates.cardRenderer = (row: T): HTMLElement => {\n // Evaluate template expressions like {{ row.field }}\n const evaluated = evalTemplateString(templateHTML, { value: row, row: row as Record<string, unknown> });\n const container = document.createElement('div');\n container.className = 'tbw-responsive-card-content';\n // Sanitize the result to prevent XSS\n setSanitizedHTML(container, evaluated);\n return container;\n };\n }\n\n // Merge updates into config (light DOM values override constructor config)\n if (Object.keys(configUpdates).length > 0) {\n this.config = { ...this.config, ...configUpdates };\n }\n }\n\n // #endregion\n\n /**\n * Build the hidden and value-only column sets from config.\n */\n #buildHiddenColumnSets(hiddenColumns?: HiddenColumnConfig[]): void {\n this.#hiddenColumnSet = new Set();\n this.#valueOnlyColumnSet.clear();\n\n for (const col of hiddenColumns ?? []) {\n if (typeof col === 'string') {\n this.#hiddenColumnSet.add(col);\n } else if (col.showValue) {\n this.#valueOnlyColumnSet.add(col.field);\n } else {\n this.#hiddenColumnSet.add(col.field);\n }\n }\n\n this.#queueHiddenColumnFades();\n }\n\n /**\n * Whether `hiddenColumns` is in effect right now: card mode drops the column\n * from the card, and in progressive degradation a matched breakpoint drops it\n * from the table. A plain table with no matching breakpoint shows everything.\n */\n get #hiddenColumnsApply(): boolean {\n return this.#sortedBreakpoints.length > 0 ? this.#activeBreakpoint !== null : this.#isResponsive;\n }\n\n /**\n * Queue a fade for every column whose visibility actually changed. The diff is\n * against what the last render hid rather than against the previous config,\n * because `hiddenColumns` is inert in a plain table — fading a column out there\n * would strand it invisible in a track that is never collapsed.\n */\n #queueHiddenColumnFades(): void {\n const applied = this.#hiddenColumnsApply ? this.#hiddenColumnSet : EMPTY_FIELD_SET;\n\n for (const field of this.#appliedHiddenFields) {\n if (!applied.has(field)) this.#markColumnFade(field, false);\n }\n for (const field of applied) {\n if (!this.#appliedHiddenFields.has(field)) this.#markColumnFade(field, true);\n }\n this.#openFadeWindow();\n }\n\n /** Queue `field` for the next fade, dropping any opposite direction still pending for it. */\n #markColumnFade(field: string, out: boolean): void {\n (out ? this.#fadeInFields : this.#fadeOutFields).delete(field);\n (out ? this.#fadeOutFields : this.#fadeInFields).add(field);\n }\n\n /**\n * Start the window during which queued columns animate. The track collapse is a\n * CSS transition on the row, which survives a render, but the opacity fade is\n * not: cells are rebuilt on every render, so a hidden column's cell would be\n * re-inserted visible and transition to hidden all over again — exactly the\n * flash of every hidden column reappearing. Cells are animated explicitly\n * instead, and only the fields that actually changed are queued.\n */\n #openFadeWindow(): void {\n const duration = this.#motionDurationMs;\n if (duration <= 0 || (this.#fadeOutFields.size === 0 && this.#fadeInFields.size === 0)) {\n this.#endFadeWindow();\n return;\n }\n\n this.#fadeEndsAt = performance.now() + duration;\n this.gridElement?.setAttribute(COLUMN_FADE_ATTR, '');\n clearTimeout(this.#fadeTimer);\n this.#fadeTimer = setTimeout(() => this.#endFadeWindow(), duration);\n }\n\n #endFadeWindow(): void {\n clearTimeout(this.#fadeTimer);\n this.#fadeTimer = undefined;\n this.#fadeEndsAt = 0;\n this.#fadeOutFields.clear();\n this.#fadeInFields.clear();\n this.gridElement?.removeAttribute(COLUMN_FADE_ATTR);\n\n // The fade-out fills forwards to hold the cell at zero until the render that\n // hides it lands. Past the window CSS owns visibility, so releasing the fill\n // is what stops an abandoned fade from stranding a visible column invisible.\n for (const cell of this.gridElement?.querySelectorAll<HTMLElement>('.cell[data-field]') ?? []) {\n cancelColumnFades(cell);\n }\n }\n\n /** Animate one cell, seeking to `elapsed` so a cell created mid-fade joins in progress. */\n #playColumnFade(cell: HTMLElement, out: boolean, duration: number, elapsed: number): void {\n cancelColumnFades(cell);\n const frames = out ? [{ opacity: 1 }, { opacity: 0 }] : [{ opacity: 0 }, { opacity: 1 }];\n // A fade-out has to hold at zero until the closing render hides the cell for real.\n const animation = cell.animate?.(frames, { duration, easing: 'ease-out', fill: out ? 'forwards' : 'none' });\n if (!animation) return;\n animation.id = COLUMN_FADE_ID;\n animation.currentTime = elapsed;\n }\n\n /**\n * Collapse the tracks of hidden columns to zero width. Cells are auto-placed, so\n * a hidden cell has to stay in layout — dropping it would pull every later cell\n * into the wrong track. Collapsing the track instead is what lets the row, and\n * therefore the horizontal scroll width, shrink to the visible columns.\n *\n * Core rewrites `--tbw-column-template` from the full column list on every\n * template update, so this override is re-applied after each render.\n */\n #applyColumnTemplate(hidden: ReadonlySet<string>): void {\n const base = this.#internalGrid?._gridTemplate ?? '';\n if (!base) return;\n\n const columns = this.visibleColumns;\n const tracks = splitTrackList(base);\n let collapsed = false;\n\n if (hidden.size > 0 && tracks.length === columns.length) {\n for (let i = 0; i < columns.length; i++) {\n const field = columns[i]?.field;\n if (field && hidden.has(field)) {\n tracks[i] = collapsedTrack(tracks[i]);\n collapsed = true;\n }\n }\n }\n\n if (!collapsed && !this.#templateOverridden) return;\n this.gridElement.style.setProperty('--tbw-column-template', collapsed ? tracks.join(' ') : base);\n this.#templateOverridden = collapsed;\n }\n\n override detach(): void {\n this.#resizeObserver?.disconnect();\n this.#resizeObserver = undefined;\n this.#endFadeWindow();\n clearTimeout(this.#debounceTimer);\n this.#debounceTimer = undefined;\n this.#lastCheckedWidth = undefined;\n this.#lastSwitchAt = Number.NEGATIVE_INFINITY;\n\n // Every attribute `#syncLayoutAttributes()` writes styles the grid on its own\n // — `[data-responsive-animate]` transitions rows even in table mode — so a\n // detached plugin that leaves one behind keeps styling a grid it no longer owns.\n if (this.gridElement) {\n this.gridElement.removeAttribute('data-responsive');\n this.gridElement.removeAttribute('data-responsive-animate');\n this.gridElement.removeAttribute('data-responsive-hide-header');\n this.gridElement.removeAttribute('data-responsive-transition');\n for (const cell of this.gridElement.querySelectorAll(\n '.cell[data-responsive-hidden], .cell[data-responsive-value-only]',\n )) {\n cell.removeAttribute('data-responsive-hidden');\n cell.removeAttribute('data-responsive-value-only');\n cell.removeAttribute('aria-hidden');\n }\n this.#clearMorphNames();\n }\n this.#appliedHiddenFields = EMPTY_FIELD_SET;\n this.#appliedValueOnlyFields = EMPTY_FIELD_SET;\n\n super.detach();\n }\n\n /**\n * Handle plugin queries.\n * @internal\n */\n override handleQuery(query: PluginQuery): unknown {\n if (query.type === 'isCardMode') {\n return this.#isResponsive;\n }\n return undefined;\n }\n\n /**\n * Apply hidden and value-only columns.\n * In legacy mode (single breakpoint), only applies when in responsive mode.\n * In multi-breakpoint mode, applies whenever there's an active breakpoint.\n */\n override afterRender(): void {\n // Measure card height for virtualization calculations\n this.#measureCardHeightFromDOM();\n\n // In single breakpoint mode, only apply when responsive\n // In multi-breakpoint mode, apply when there's an active breakpoint\n const shouldApply = this.#sortedBreakpoints.length > 0 ? this.#activeBreakpoint !== null : this.#isResponsive;\n\n const hidden = shouldApply ? this.#hiddenColumnSet : EMPTY_FIELD_SET;\n const valueOnly = shouldApply ? this.#valueOnlyColumnSet : EMPTY_FIELD_SET;\n const fading = this.#fadeOutFields.size > 0 || this.#fadeInFields.size > 0;\n\n // Cells live in a recycled row pool, so an attribute written in card mode\n // outlives the switch back to table — one clearing pass is still owed.\n const stale = this.#appliedHiddenFields.size > 0 || this.#appliedValueOnlyFields.size > 0;\n\n // A layout switch changes which columns are hidden without going through the\n // config, and the switch animation already covers it — so record the new\n // truth here rather than letting the next config change diff against a stale set.\n this.#appliedHiddenFields = hidden;\n this.#appliedValueOnlyFields = valueOnly;\n\n // Card mode stacks cells vertically, so the track list is meaningless there.\n this.#applyColumnTemplate(this.#isResponsive ? EMPTY_FIELD_SET : hidden);\n\n if (hidden.size === 0 && valueOnly.size === 0 && !fading && !stale) {\n return;\n }\n\n const duration = this.#motionDurationMs;\n const elapsed = fading ? Math.min(duration, Math.max(0, duration - (this.#fadeEndsAt - performance.now()))) : 0;\n\n // Mark cells for hidden columns and value-only columns\n const cells = this.gridElement.querySelectorAll<HTMLElement>('.cell[data-field]');\n for (const cell of cells) {\n const field = cell.getAttribute('data-field');\n if (!field) continue;\n\n const fadingOut = this.#fadeOutFields.has(field);\n if (fadingOut || this.#fadeInFields.has(field)) this.#playColumnFade(cell, fadingOut, duration, elapsed);\n\n // The cell keeps its box and collapses with its track, so it stays out of\n // the accessibility tree the way `display: none` used to keep it.\n if (hidden.has(field)) {\n cell.setAttribute('data-responsive-hidden', '');\n cell.setAttribute('aria-hidden', 'true');\n cell.removeAttribute('data-responsive-value-only');\n }\n // Apply value-only attribute (shows value without header label)\n else if (valueOnly.has(field)) {\n cell.setAttribute('data-responsive-value-only', '');\n cell.removeAttribute('data-responsive-hidden');\n cell.removeAttribute('aria-hidden');\n }\n // Clear any previous responsive attributes\n else {\n cell.removeAttribute('data-responsive-hidden');\n cell.removeAttribute('data-responsive-value-only');\n cell.removeAttribute('aria-hidden');\n }\n }\n }\n\n /**\n * Check if width has crossed any breakpoint threshold.\n * Handles both single breakpoint (legacy) and multi-breakpoint modes.\n */\n #checkBreakpoint(width: number): void {\n // Multi-breakpoint mode\n if (this.#sortedBreakpoints.length > 0) {\n this.#checkMultiBreakpoint(width);\n return;\n }\n\n // Legacy single breakpoint mode\n const breakpoint = this.config.breakpoint ?? 0;\n\n // Warn once if breakpoint not configured (0 means never responsive)\n if (breakpoint === 0 && !this.#warnedAboutMissingBreakpoint) {\n this.#warnedAboutMissingBreakpoint = true;\n this.warn(\n MISSING_BREAKPOINT,\n \"No breakpoint configured. Responsive mode is disabled. Set a breakpoint based on your grid's column count.\",\n );\n }\n\n const shouldBeResponsive = breakpoint > 0 && width < breakpoint;\n\n if (shouldBeResponsive !== this.#isResponsive) {\n this.#isResponsive = shouldBeResponsive;\n this.#lastSwitchAt = performance.now();\n this.#commitLayoutSwitch();\n this.emit('responsive-change', {\n isResponsive: shouldBeResponsive,\n width,\n breakpoint,\n } satisfies ResponsiveChangeDetail);\n }\n }\n\n /**\n * Check breakpoints in multi-breakpoint mode.\n * Evaluates breakpoints from largest to smallest, applying the first match.\n */\n #checkMultiBreakpoint(width: number): void {\n // Find the active breakpoint (first one where width <= maxWidth)\n // Since sorted largest to smallest, we find the largest matching breakpoint\n let newActiveBreakpoint: BreakpointConfig | null = null;\n\n for (const bp of this.#sortedBreakpoints) {\n if (width <= bp.maxWidth) {\n newActiveBreakpoint = bp;\n // Continue to find the most specific (smallest) matching breakpoint\n }\n }\n\n // Check if breakpoint changed\n const breakpointChanged = newActiveBreakpoint !== this.#activeBreakpoint;\n\n if (breakpointChanged) {\n this.#activeBreakpoint = newActiveBreakpoint;\n this.#lastSwitchAt = performance.now();\n\n // Update hidden column sets from active breakpoint\n if (newActiveBreakpoint?.hiddenColumns) {\n this.#buildHiddenColumnSets(newActiveBreakpoint.hiddenColumns);\n } else {\n // Fall back to top-level hiddenColumns config\n this.#buildHiddenColumnSets(this.config.hiddenColumns);\n }\n\n // Determine if we should be in card layout\n const shouldBeResponsive = newActiveBreakpoint?.cardLayout === true;\n const layoutChanged = shouldBeResponsive !== this.#isResponsive;\n\n if (layoutChanged) {\n this.#isResponsive = shouldBeResponsive;\n }\n\n // A hidden-column change still needs a re-render even when the card\n // layout itself did not flip, so always commit through the same path.\n this.#commitLayoutSwitch(layoutChanged);\n\n // Emit event for any breakpoint change\n this.emit('responsive-change', {\n isResponsive: this.#isResponsive,\n width,\n breakpoint: newActiveBreakpoint?.maxWidth ?? 0,\n } satisfies ResponsiveChangeDetail);\n }\n }\n\n /**\n * Commit a layout switch, driving it through a view transition when the\n * browser supports element-scoped transitions.\n *\n * The switch is a wholesale layout change (table rows become cards), so the\n * compositor cross-fade is both cheaper and more legible than animating the\n * DOM. Where `Element.startViewTransition()` is unavailable the grid falls\n * back to the CSS keyframe fade driven by `data-responsive-animate`.\n *\n * A hidden-column change is deliberately excluded from the transition. Only the\n * affected column moves in that case — its grid track collapses to zero width\n * while every other track keeps its size — so the leaving cell can fade and\n * shrink in place. Snapshotting the whole grid for it makes all columns shimmer,\n * and because the change is driven by a resize the frozen snapshot is scaled\n * against a container that is still being dragged.\n * @param layoutChanged - Whether the table/card layout itself flipped. `false`\n * when only the hidden-column set changed.\n */\n #commitLayoutSwitch(layoutChanged = true): void {\n this.#syncMotionDuration();\n\n // The view transition already cross-fades everything, so per-column fades\n // would only fight it.\n if (layoutChanged) this.#endFadeWindow();\n\n const startViewTransition = layoutChanged ? this.#viewTransitionStarter() : null;\n if (!startViewTransition) {\n if (layoutChanged) this.#applyResponsiveState();\n this.requestRender();\n return;\n }\n\n const host = this.gridElement;\n const morph = this.#resolveMorphGranularity();\n const token = ++this.#transitionToken;\n\n // Names must exist on the outgoing rows before the snapshot is taken.\n if (morph !== 'none') this.#setMorphNames(morph);\n host.setAttribute('data-responsive-transition', '');\n\n const transition = startViewTransition(async () => {\n this.#applyResponsiveState();\n // The render scheduler batches into rAF, which still runs inside the\n // update callback — so awaiting here captures the finished new layout.\n await this.#internalGrid.forceLayout?.();\n if (morph !== 'none') this.#setMorphNames(morph);\n });\n\n const cleanup = (): void => {\n // A superseded transition still settles, and tearing down the names and\n // attribute its successor is mid-flight on would abort that successor.\n if (token !== this.#transitionToken) return;\n host.removeAttribute('data-responsive-transition');\n if (morph !== 'none') this.#clearMorphNames();\n };\n transition.finished.then(cleanup, cleanup);\n // A drag across the breakpoint supersedes the transition in flight; the UA\n // skips it and rejects `ready`, which is expected rather than an error.\n transition.ready?.catch(() => undefined);\n }\n\n /** Animation style in effect, resolving the deprecated `animate` flag. */\n get #animation(): ResponsiveAnimation {\n const { animation, animate } = this.config;\n if (animation !== undefined) return animation;\n return animate === false ? false : 'fade';\n }\n\n /** Duration every responsive animation runs at; `0` when motion is suppressed. */\n get #motionDurationMs(): number {\n const enabled = this.#animation !== false && this.isAnimationEnabled;\n return enabled ? (this.config.animationDuration ?? 200) : 0;\n }\n\n /**\n * Publish the single duration every responsive animation reads. Collapsing it\n * to `0ms` is how `animation: false` and `prefers-reduced-motion` silence the\n * CSS-driven paths (the column fade, the card-enter keyframe, and the view\n * transition groups), none of which can see the plugin config.\n */\n #syncMotionDuration(): void {\n this.gridElement.style.setProperty('--tbw-responsive-duration', `${this.#motionDurationMs}ms`);\n }\n\n /**\n * Resolve the element-scoped view transition entry point, or `null` when\n * animation is disabled or the browser does not support it.\n */\n #viewTransitionStarter(): ViewTransitionStarter | null {\n if (this.#animation === false || !this.isAnimationEnabled) return null;\n\n const host: ViewTransitionCapableElement = this.gridElement;\n const start = host.startViewTransition;\n return typeof start === 'function' ? start.bind(host) : null;\n }\n\n /** Whether the browser can drive the layout switch on the compositor. */\n get #supportsViewTransition(): boolean {\n const host: ViewTransitionCapableElement | undefined = this.gridElement;\n return typeof host?.startViewTransition === 'function';\n }\n\n /**\n * Give every rendered row a stable `view-transition-name` so the compositor\n * animates each row from its table position to its card position.\n */\n #setMorphNames(granularity: MorphGranularity): void {\n const rows = this.gridElement.querySelectorAll<HTMLElement>('.data-grid-row[aria-rowindex]');\n for (const row of rows) {\n const rowIndex = row.getAttribute('aria-rowindex');\n if (granularity === 'rows') {\n row.style.setProperty('view-transition-name', `${this.#morphPrefix}${rowIndex}`);\n continue;\n }\n // Only the leaves are named — naming the row too would lift its cells out\n // of the row's own snapshot and animate both against each other.\n for (const cell of row.querySelectorAll<HTMLElement>(':scope > .cell[aria-colindex]')) {\n const name = `${this.#cellMorphPrefix}${rowIndex}-${cell.getAttribute('aria-colindex')}`;\n cell.style.setProperty('view-transition-name', name);\n }\n }\n }\n\n /**\n * Pick the morph granularity for this switch.\n *\n * Cell identity is NOT stable across a layout switch (the row renderer\n * rebuilds cells whenever the visible column set changes), so names are\n * authored rather than left to `view-transition-name: match-element`.\n */\n #resolveMorphGranularity(): MorphGranularity {\n const animation = this.#animation;\n if (animation === 'morph-cells') {\n const cells = this.gridElement.querySelectorAll('.data-grid-row > .cell[aria-colindex]').length;\n if (cells > 0 && cells <= MAX_MORPH_CELLS) return 'cells';\n return 'rows';\n }\n return animation === 'morph-rows' ? 'rows' : 'none';\n }\n\n /** Remove morph names so recycled rows and cells do not carry stale identities. */\n #clearMorphNames(): void {\n const named = this.gridElement.querySelectorAll<HTMLElement>('.data-grid-row, .data-grid-row > .cell');\n for (const el of named) {\n el.style.removeProperty('view-transition-name');\n }\n }\n\n /** Original row height before entering responsive mode, for restoration on exit */\n #originalRowHeight?: number;\n\n /** The host attributes every responsive CSS rule keys off. */\n #syncLayoutAttributes(): void {\n this.gridElement.toggleAttribute('data-responsive', this.#isResponsive);\n\n // The keyframe fade is the no-view-transition path only. Leaving it on\n // would replay the card-enter animation the moment the transition ends.\n this.gridElement.toggleAttribute(\n 'data-responsive-animate',\n this.#animation !== false && !this.#supportsViewTransition,\n );\n\n // Attribute only meaningful while in card mode; clear it otherwise so\n // the CSS rule cannot accidentally apply to non-responsive layouts.\n this.gridElement.toggleAttribute(\n 'data-responsive-hide-header',\n this.#isResponsive && this.config.hideHeader === true,\n );\n }\n\n /**\n * Apply the responsive state to the grid element.\n * Handles scroll reset when entering responsive mode and row height restoration on exit.\n */\n #applyResponsiveState(): void {\n this.#syncLayoutAttributes();\n\n // Cast to internal type for virtualization access\n const internalGrid = this.#internalGrid;\n\n if (this.#isResponsive) {\n // Store original row height before responsive mode changes it\n if (internalGrid._virtualization) {\n this.#originalRowHeight = internalGrid._virtualization.rowHeight;\n }\n\n // Reset horizontal scroll position when entering responsive mode\n // The CSS hides overflow but doesn't reset the scroll position\n const scrollArea = this.gridElement.querySelector('.tbw-scroll-area') as HTMLElement | null;\n if (scrollArea) {\n scrollArea.scrollLeft = 0;\n }\n } else {\n // Exiting responsive mode - clean up inline styles set by renderRow\n // The rows are reused from the pool, so we need to remove the card-specific styles\n const rows = this.gridElement.querySelectorAll('.data-grid-row');\n for (const row of rows) {\n (row as HTMLElement).style.height = '';\n row.classList.remove('responsive-card');\n }\n\n // Restore original row height\n if (this.#originalRowHeight && this.#originalRowHeight > 0 && internalGrid._virtualization) {\n internalGrid._virtualization.rowHeight = this.#originalRowHeight;\n this.#originalRowHeight = undefined;\n }\n\n // Clear cached measurements so they're remeasured fresh when re-entering responsive mode\n // Without this, stale measurements cause incorrect height calculations after scrolling\n this.#measuredCardHeight = undefined;\n this.#measuredGroupRowHeight = undefined;\n this.#lastCardRowCount = undefined;\n }\n }\n\n /**\n * Custom row rendering when cardRenderer is provided and in responsive mode.\n *\n * When a cardRenderer is configured, this hook takes over row rendering to display\n * the custom card layout instead of the default cell structure.\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 rendered (prevents default), `void` for default rendering\n */\n override renderRow(row: unknown, rowEl: HTMLElement, rowIndex: number): boolean | void {\n // Only override when in responsive mode AND cardRenderer is provided\n if (!this.#isResponsive || !this.config.cardRenderer) {\n return; // Let default rendering proceed\n }\n\n // Skip group rows from GroupingRowsPlugin - they have special structure\n // and should use their own renderer\n if ((row as { __isGroupRow?: boolean }).__isGroupRow) {\n return; // Let GroupingRowsPlugin handle group row rendering\n }\n\n // Clear existing content\n rowEl.replaceChildren();\n\n // Call user's cardRenderer to get custom content\n const cardContent = this.config.cardRenderer(row as T, rowIndex);\n\n // Reset className - clears any stale classes from previous use (e.g., 'group-row' from recycled element)\n // This follows the same pattern as GroupingRowsPlugin which sets className explicitly\n rowEl.className = 'data-grid-row responsive-card';\n\n // Handle card row height — when a numeric height is configured, use the effective\n // height from #getCardHeight() which incorporates DOM measurement after first render.\n // This keeps inline height in sync with the position cache used for virtualization.\n const configuredHeight = this.config.cardRowHeight;\n if (configuredHeight === 'auto' || configuredHeight === undefined) {\n rowEl.style.height = 'auto';\n } else {\n rowEl.style.height = `${this.#getCardHeight()}px`;\n }\n\n // Append the custom card content\n // The row keeps `role=\"row\"`, which may only own cell-ish children (WCAG\n // 1.3.1 / axe `aria-required-children`). A card collapses the whole record\n // into one box, so it is exposed as a single spanning gridcell — unless the\n // renderer already assigned its own role.\n if (!cardContent.hasAttribute('role')) {\n cardContent.setAttribute('role', 'gridcell');\n cardContent.setAttribute('aria-colindex', '1');\n }\n rowEl.appendChild(cardContent);\n\n return true; // We handled rendering\n }\n\n /**\n * Handle keyboard navigation in responsive mode.\n *\n * In responsive mode, the visual layout is inverted:\n * - Cells are stacked vertically within each \"card\" (row)\n * - DOWN/UP visually moves within the card (between fields)\n * - Page Down/Page Up or Ctrl+Down/Up moves between cards\n *\n * For custom cardRenderers, keyboard navigation is disabled entirely\n * since the implementor controls the card content and should handle\n * navigation via their own event handlers.\n *\n * @returns `true` if the event was handled and default behavior should be prevented\n */\n override onKeyDown(e: KeyboardEvent): boolean {\n if (!this.#isResponsive) {\n return false;\n }\n\n // If custom cardRenderer is provided, disable grid's keyboard navigation\n // The implementor is responsible for their own navigation\n if (this.config.cardRenderer) {\n const navKeys = ['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'];\n if (navKeys.includes(e.key)) {\n // Let the event bubble - implementor can handle it\n return false;\n }\n }\n\n // Swap arrow key behavior for CSS-only responsive mode\n // In card layout, cells are stacked vertically:\n // Card 1: Card 2:\n // ID: 1 ID: 2\n // Name: Alice Name: Bob <- ArrowRight goes here\n // Dept: Eng Dept: Mkt\n // ↓ ArrowDown goes here\n //\n // ArrowDown/Up = move within card (change column/field)\n // ArrowRight/Left = move between cards (change row)\n const maxRow = this.rows.length - 1;\n const maxCol = this.visibleColumns.length - 1;\n\n switch (e.key) {\n case 'ArrowDown':\n // Move down WITHIN card (to next field/column)\n if (this.grid._focusCol < maxCol) {\n this.grid._focusCol += 1;\n e.preventDefault();\n ensureCellVisible(this.#internalGrid);\n return true;\n }\n // At bottom of card - optionally move to next card's first field\n if (this.grid._focusRow < maxRow) {\n this.grid._focusRow += 1;\n this.grid._focusCol = 0;\n e.preventDefault();\n ensureCellVisible(this.#internalGrid);\n return true;\n }\n break;\n\n case 'ArrowUp':\n // Move up WITHIN card (to previous field/column)\n if (this.grid._focusCol > 0) {\n this.grid._focusCol -= 1;\n e.preventDefault();\n ensureCellVisible(this.#internalGrid);\n return true;\n }\n // At top of card - optionally move to previous card's last field\n if (this.grid._focusRow > 0) {\n this.grid._focusRow -= 1;\n this.grid._focusCol = maxCol;\n e.preventDefault();\n ensureCellVisible(this.#internalGrid);\n return true;\n }\n break;\n\n case 'ArrowRight':\n // Move to NEXT card (same field)\n if (this.grid._focusRow < maxRow) {\n this.grid._focusRow += 1;\n e.preventDefault();\n ensureCellVisible(this.#internalGrid);\n return true;\n }\n break;\n\n case 'ArrowLeft':\n // Move to PREVIOUS card (same field)\n if (this.grid._focusRow > 0) {\n this.grid._focusRow -= 1;\n e.preventDefault();\n ensureCellVisible(this.#internalGrid);\n return true;\n }\n break;\n }\n\n return false;\n }\n\n // ============================================\n // Variable Height Support for Mixed Row Types\n // ============================================\n\n /** Measured card height from DOM for virtualization calculations */\n #measuredCardHeight?: number;\n\n /** Measured group row height from DOM for virtualization calculations */\n #measuredGroupRowHeight?: number;\n\n /** Last known card row count for detecting changes (e.g., group expand/collapse) */\n #lastCardRowCount?: number;\n\n /**\n * Get the effective card height for virtualization calculations.\n */\n #getCardHeight(): number {\n const configHeight = this.config.cardRowHeight;\n const measured = this.#measuredCardHeight;\n\n // A fixed height is the answer unless the content overflows it; only `auto`\n // hands the decision to the measurement outright.\n if (typeof configHeight === 'number' && configHeight > 0) {\n return measured && measured > configHeight ? measured : configHeight;\n }\n if (measured && measured > 0) {\n return measured;\n }\n return 80;\n }\n\n /**\n * Get the effective group row height for virtualization calculations.\n * Uses DOM-measured height, falling back to original row height.\n */\n #getGroupRowHeight(): number {\n if (this.#measuredGroupRowHeight && this.#measuredGroupRowHeight > 0) {\n return this.#measuredGroupRowHeight;\n }\n // Fall back to original row height (before responsive mode)\n return this.#originalRowHeight ?? 28;\n }\n\n /**\n * Check if there are any group rows in the current dataset.\n * Used to determine if we have mixed row heights.\n */\n #hasGroupRows(): boolean {\n for (const row of this.rows) {\n if ((row as { __isGroupRow?: boolean }).__isGroupRow) {\n return true;\n }\n }\n return false;\n }\n\n /**\n * Get the height of a specific row based on its type (group row vs card row).\n * Returns undefined if not in responsive mode.\n *\n * @param row - The row data\n * @param _index - The row index (unused, but part of the interface)\n * @returns The row height in pixels, or undefined if not in responsive mode\n */\n override getRowHeight(row: unknown, _index: number): number | undefined {\n // Only applies when in responsive mode with cardRenderer\n if (!this.#isResponsive || !this.config.cardRenderer) {\n return undefined;\n }\n\n // Check if this is a group row\n if ((row as { __isGroupRow?: boolean }).__isGroupRow) {\n return this.#getGroupRowHeight();\n }\n\n // Regular card row\n return this.#getCardHeight();\n }\n\n /**\n * Count the number of card rows (non-group rows) in the current dataset.\n */\n #countCardRows(): number {\n let count = 0;\n for (const row of this.rows) {\n if (!(row as { __isGroupRow?: boolean }).__isGroupRow) {\n count++;\n }\n }\n return count;\n }\n\n /** Pending refresh scheduled via microtask */\n #pendingRefresh = false;\n\n /**\n * Measure card height from DOM after render and detect row count changes.\n * Called in afterRender to ensure scroll calculations are accurate.\n *\n * This handles two scenarios:\n * 1. Card height changes (content overflow, dynamic sizing)\n * 2. Card row count changes (group expand/collapse)\n * 3. Group row height changes\n *\n * For uniform card layouts (no groups), we update the virtualization row height\n * directly to the card height. For mixed layouts (groups + cards), we use the\n * getExtraHeight mechanism to report height differences.\n *\n * The refresh is deferred via microtask to avoid nested render cycles.\n */\n #measureCardHeightFromDOM(): void {\n if (!this.#isResponsive) {\n return;\n }\n\n let needsRefresh = false;\n const internalGrid = this.#internalGrid;\n const hasGroups = this.#hasGroupRows();\n\n // Check if card row count changed (e.g., group expanded/collapsed)\n const currentCardRowCount = this.#countCardRows();\n if (currentCardRowCount !== this.#lastCardRowCount) {\n this.#lastCardRowCount = currentCardRowCount;\n needsRefresh = true;\n }\n\n // Measure actual group row height from DOM (for mixed layouts)\n if (hasGroups) {\n const groupRow = this.gridElement.querySelector('.data-grid-row.group-row') as HTMLElement | null;\n if (groupRow) {\n const height = groupRow.getBoundingClientRect().height;\n if (height > 0 && height !== this.#measuredGroupRowHeight) {\n this.#measuredGroupRowHeight = height;\n needsRefresh = true;\n }\n }\n }\n\n // Measure actual card height from DOM.\n // With cardRenderer, rows have the `.responsive-card` class.\n // Without cardRenderer (CSS-only card mode), rows are plain `.data-grid-row`\n // elements whose CSS height is `auto` — we need to measure their actual\n // rendered height so the faux scrollbar spacer is sized correctly.\n const cardSelector = this.config.cardRenderer ? '.data-grid-row.responsive-card' : '.data-grid-row:not(.group-row)';\n const cardRow = this.gridElement.querySelector(cardSelector) as HTMLElement | null;\n if (cardRow) {\n const height = cardRow.getBoundingClientRect().height;\n if (height > 0 && height !== this.#measuredCardHeight) {\n this.#measuredCardHeight = height;\n needsRefresh = true;\n\n // For uniform card layouts (no groups), update virtualization row height directly\n // This ensures proper row recycling and translateY calculations\n if (!hasGroups && internalGrid._virtualization) {\n internalGrid._virtualization.rowHeight = height;\n }\n }\n }\n\n // Defer virtualization refresh to avoid nested render cycles\n // This is called from afterRender, so we can't call refreshVirtualWindow synchronously\n // Use scheduler's VIRTUALIZATION phase to batch properly and avoid duplicate afterRender calls\n if (needsRefresh && !this.#pendingRefresh) {\n this.#pendingRefresh = true;\n queueMicrotask(() => {\n this.#pendingRefresh = false;\n // Only refresh if still attached and in responsive mode\n if (this.grid && this.#isResponsive) {\n // Request virtualization phase through grid's public API\n // This goes through the scheduler which batches and handles afterRender properly\n this.#internalGrid.refreshVirtualWindow?.(true, true);\n }\n });\n }\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../../../../../libs/grid/src/lib/core/internal/diagnostics.ts","../../../../../../libs/grid/src/lib/core/constants.ts","../../../../../../libs/grid/src/lib/core/internal/utils.ts","../../../../../../libs/grid/src/lib/core/internal/virtualization.ts","../../../../../../libs/grid/src/lib/core/internal/keyboard.ts","../../../../../../libs/grid/src/lib/core/internal/sanitize.ts","../../../../../../libs/grid/src/lib/core/defaults.ts","../../../../../../libs/grid/src/lib/core/plugin/base-plugin.ts","../../../../../../libs/grid/src/lib/plugins/responsive/responsive.css?inline","../../../../../../libs/grid/src/lib/plugins/responsive/responsive-plugin.ts"],"sourcesContent":["/**\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 * 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","// #region Environment Helpers\n\n/**\n * Check if we're running in a development environment.\n * Returns true for localhost or when NODE_ENV !== 'production'.\n * Used to show warnings only in development.\n */\nexport function isDevelopment(): boolean {\n // Check for localhost (browser environment)\n if (typeof window !== 'undefined' && window.location) {\n const hostname = window.location.hostname;\n if (hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1') {\n return true;\n }\n }\n // Check for NODE_ENV (build-time or SSR)\n if (typeof process !== 'undefined' && process.env?.NODE_ENV !== 'production') {\n return true;\n }\n return false;\n}\n\n// #endregion\n\n// #region Cell Rendering Helpers\n\n/**\n * Generate accessible HTML for a boolean cell.\n * Uses role=\"checkbox\" with proper aria attributes.\n */\nexport function booleanCellHTML(value: boolean): string {\n return `<span role=\"checkbox\" aria-checked=\"${value}\" aria-label=\"${value}\">${value ? '🗹' : '☐'}</span>`;\n}\n\n/**\n * Format a date value for display.\n * Handles Date objects, timestamps, and date strings.\n * Returns empty string for invalid dates.\n */\nexport function formatDateValue(value: unknown): string {\n if (value == null || value === '') return '';\n if (value instanceof Date) {\n return isNaN(value.getTime()) ? '' : value.toLocaleDateString();\n }\n if (typeof value === 'number' || typeof value === 'string') {\n const d = new Date(value);\n return isNaN(d.getTime()) ? '' : d.toLocaleDateString();\n }\n return '';\n}\n\n/**\n * Get the row index from a cell element's data-row attribute.\n * Falls back to calculating from parent row's DOM position if data-row is missing.\n * Returns -1 if no valid row index is found.\n */\nexport function getRowIndexFromCell(cell: Element | null): number {\n if (!cell) return -1;\n const attr = cell.getAttribute('data-row');\n if (attr) return parseInt(attr, 10);\n\n // Fallback: find the parent .data-grid-row and calculate index from siblings\n const rowEl = cell.closest('.data-grid-row');\n if (!rowEl) return -1;\n\n const parent = rowEl.parentElement;\n if (!parent) return -1;\n\n // Get all data-grid-row siblings and find this row's index\n const rows = parent.querySelectorAll(':scope > .data-grid-row');\n for (let i = 0; i < rows.length; i++) {\n if (rows[i] === rowEl) return i;\n }\n return -1;\n}\n\n/**\n * Get the column index from a cell element's data-col attribute.\n * Returns -1 if no valid column index is found.\n */\nexport function getColIndexFromCell(cell: Element | null): number {\n if (!cell) return -1;\n const attr = cell.getAttribute('data-col');\n return attr ? parseInt(attr, 10) : -1;\n}\n\n/**\n * Clear all cell-focus styling from a root element.\n * Used when changing focus or when selection plugin takes over focus management.\n */\nexport function clearCellFocus(root: Element | null): void {\n if (!root) return;\n root.querySelectorAll('.cell-focus').forEach((el) => el.classList.remove('cell-focus'));\n}\n// #endregion\n\n// #region RTL Helpers\n\n/** Text direction */\nexport type TextDirection = 'ltr' | 'rtl';\n\n/**\n * Get the text direction for an element.\n * Reads from the computed style, which respects the `dir` attribute on the element\n * or any ancestor, as well as CSS `direction` property.\n *\n * @param element - The element to check direction for\n * @returns 'ltr' or 'rtl'\n *\n * @example\n * ```typescript\n * // Detect grid's direction\n * const dir = getDirection(gridElement);\n * if (dir === 'rtl') {\n * // Handle RTL layout\n * }\n * ```\n */\nexport function getDirection(element: Element): TextDirection {\n // Try computed style first (works in real browsers)\n try {\n const computedDir = getComputedStyle(element).direction;\n if (computedDir === 'rtl') return 'rtl';\n } catch {\n // getComputedStyle may fail in some test environments\n }\n\n // Fallback: check dir attribute on element or ancestors\n // This handles test environments where getComputedStyle may not reflect dir attribute\n try {\n const dirAttr = element.closest?.('[dir]')?.getAttribute('dir');\n if (dirAttr === 'rtl') return 'rtl';\n } catch {\n // closest may not be available on mock elements\n }\n\n return 'ltr';\n}\n\n/**\n * Check if an element is in RTL mode.\n *\n * @param element - The element to check\n * @returns true if the element's text direction is right-to-left\n */\nexport function isRTL(element: Element): boolean {\n return getDirection(element) === 'rtl';\n}\n\n/**\n * Resolve a logical inline position to a physical position based on text direction.\n *\n * - `'start'` → `'left'` in LTR, `'right'` in RTL\n * - `'end'` → `'right'` in LTR, `'left'` in RTL\n * - `'left'` / `'right'` → unchanged (physical values)\n *\n * @param position - Logical or physical position\n * @param direction - Text direction ('ltr' or 'rtl')\n * @returns Physical position ('left' or 'right')\n *\n * @example\n * ```typescript\n * resolveInlinePosition('start', 'ltr'); // 'left'\n * resolveInlinePosition('start', 'rtl'); // 'right'\n * resolveInlinePosition('left', 'rtl'); // 'left' (unchanged)\n * ```\n */\nexport function resolveInlinePosition(\n position: 'left' | 'right' | 'start' | 'end',\n direction: TextDirection,\n): 'left' | 'right' {\n if (position === 'left' || position === 'right') {\n return position;\n }\n if (direction === 'rtl') {\n return position === 'start' ? 'right' : 'left';\n }\n return position === 'start' ? 'left' : 'right';\n}\n// #endregion\n","/**\n * Row Virtualization Module\n *\n * Provides all virtualization-related functionality for the grid:\n *\n * 1. **Position Cache** (index-based): Maps row index → {offset, height, measured}\n * - Rebuilt when row count changes (expand/collapse, filter)\n * - Used for scroll position → row index lookups (binary search)\n *\n * 2. **Height Cache** (identity-based): Maps row identity → height\n * - Persists across expand/collapse\n * - Uses rowId string keys when available, WeakMap otherwise\n * - Synthetic rows use __rowCacheKey for stable identity\n *\n * 3. **Virtual Window**: Computes which rows to render based on scroll position\n *\n * 4. **Row Measurement**: Measures rendered row heights from DOM\n */\n\n// #region Types\n\n/**\n * Position entry for a single row in the position cache.\n * @category Plugin Development\n */\nexport interface RowPosition {\n /** Cumulative offset from top in pixels */\n offset: number;\n /** Row height in pixels */\n height: number;\n /** Whether this is a measured value (true) or estimate (false) */\n measured: boolean;\n}\n\n/**\n * Height cache that persists row heights across position cache rebuilds.\n * Uses dual storage: Map for string keys (rowId, __rowCacheKey) and WeakMap for object refs.\n * @category Plugin Development\n */\nexport interface HeightCache {\n /** Heights keyed by string (for synthetic rows with __rowCacheKey or rowId-keyed rows) */\n byKey: Map<string, number>;\n /** Heights keyed by object reference (for data rows without rowId) */\n byRef: WeakMap<object, number>;\n}\n\n/**\n * Configuration for the position cache.\n */\nexport interface PositionCacheConfig<T = unknown> {\n /** Function to get row ID (if configured) */\n rowId?: (row: T) => string | number;\n}\n\n// #endregion\n\n// #region Height Cache Operations\n\n/**\n * Create a new empty height cache.\n */\nexport function createHeightCache(): HeightCache {\n return {\n byKey: new Map(),\n byRef: new WeakMap(),\n };\n}\n\n/**\n * Get the cache key for a row.\n * Returns string for synthetic rows (__rowCacheKey) or rowId-keyed rows,\n * or the row object itself for WeakMap lookup.\n */\nexport function getRowCacheKey<T>(row: T, rowId?: (row: T) => string | number): string | T {\n if (!row || typeof row !== 'object') return row;\n\n // 1. Synthetic rows: plugins MUST add __rowCacheKey\n if ('__rowCacheKey' in row) {\n return (row as { __rowCacheKey: string }).__rowCacheKey;\n }\n\n // 2. rowId property directly on the row (common pattern)\n if ('rowId' in row && (row as { rowId: string | number }).rowId != null) {\n return `id:${(row as { rowId: string | number }).rowId}`;\n }\n\n // 3. User-provided rowId function (if configured)\n if (rowId) {\n return `id:${rowId(row)}`;\n }\n\n // 4. Object identity (for WeakMap)\n return row;\n}\n\n/**\n * Get cached height for a row.\n * @returns Cached height or undefined if not cached\n */\nexport function getCachedHeight<T>(\n cache: HeightCache,\n row: T,\n rowId?: (row: T) => string | number,\n): number | undefined {\n const key = getRowCacheKey(row, rowId);\n\n if (typeof key === 'string') {\n return cache.byKey.get(key);\n }\n\n // Object key - use WeakMap\n if (key && typeof key === 'object') {\n return cache.byRef.get(key);\n }\n\n return undefined;\n}\n\n/**\n * Set cached height for a row.\n */\nexport function setCachedHeight<T>(\n cache: HeightCache,\n row: T,\n height: number,\n rowId?: (row: T) => string | number,\n): void {\n const key = getRowCacheKey(row, rowId);\n\n if (typeof key === 'string') {\n cache.byKey.set(key, height);\n } else if (key && typeof key === 'object') {\n cache.byRef.set(key, height);\n }\n}\n\n// #endregion\n\n// #region Position Cache Operations\n\n/**\n * Rebuild position cache preserving known heights from height cache.\n * Called when row count changes (expand/collapse, filter, data change).\n *\n * @param rows - Array of row data\n * @param heightCache - Height cache with persisted measurements\n * @param estimatedHeight - Estimated height for unmeasured rows\n * @param config - Position cache configuration\n * @param getPluginHeight - Optional function to get height from plugins\n * @returns New position cache\n */\nexport function rebuildPositionCache<T>(\n rows: T[],\n heightCache: HeightCache,\n estimatedHeight: number,\n config: PositionCacheConfig<T>,\n getPluginHeight?: (row: T, index: number) => number | undefined,\n): RowPosition[] {\n const cache: RowPosition[] = new Array(rows.length);\n let offset = 0;\n\n for (let i = 0; i < rows.length; i++) {\n const row = rows[i];\n\n // Height resolution order:\n // 1. Plugin's getRowHeight() (for synthetic rows with known heights)\n let height = getPluginHeight?.(row, i);\n let measured = height !== undefined;\n\n // 2. Cached height from previous measurements\n if (height === undefined) {\n height = getCachedHeight(heightCache, row, config.rowId);\n measured = height !== undefined;\n }\n\n // 3. Fall back to estimate\n if (height === undefined) {\n height = estimatedHeight;\n measured = false;\n }\n\n cache[i] = { offset, height, measured };\n offset += height;\n }\n\n return cache;\n}\n\n/**\n * Update a single row's height in the position cache.\n * Recalculates offsets for all subsequent rows.\n *\n * @param cache - Position cache to update\n * @param index - Row index to update\n * @param newHeight - New measured height\n */\nexport function updateRowHeight(cache: RowPosition[], index: number, newHeight: number): void {\n if (index < 0 || index >= cache.length) return;\n\n const entry = cache[index];\n const heightDiff = newHeight - entry.height;\n\n // Latch `measured` even when the height is unchanged, otherwise callers that enqueue on\n // `!measured` re-measure the same row on every render pass.\n entry.measured = true;\n\n if (heightDiff === 0) return;\n\n // Update this row\n entry.height = newHeight;\n\n // Recalculate offsets for all subsequent rows\n for (let i = index + 1; i < cache.length; i++) {\n cache[i].offset += heightDiff;\n }\n}\n\nexport interface RowHeightChange {\n /** Row index into the position cache. */\n index: number;\n /** Measured height in pixels. */\n height: number;\n}\n\n/**\n * Update multiple row heights while recalculating the affected cache suffix once.\n *\n * @param cache - Position cache to update (mutated in place)\n * @param changes - Height changes in any order; each row index should occur at most once\n */\nexport function updateRowHeights(cache: RowPosition[], changes: RowHeightChange[]): void {\n if (changes.length === 0 || cache.length === 0) return;\n\n // Callers usually collect in DOM order, which is already ascending — skip the copy then.\n let sortedChanges = changes;\n for (let i = 1; i < changes.length; i++) {\n if (changes[i].index < changes[i - 1].index) {\n sortedChanges = [...changes].sort((a, b) => a.index - b.index);\n break;\n }\n }\n\n let changeIndex = 0;\n while (changeIndex < sortedChanges.length && sortedChanges[changeIndex].index < 0) changeIndex++;\n if (changeIndex >= sortedChanges.length || sortedChanges[changeIndex].index >= cache.length) return;\n\n let cumulativeDiff = 0;\n const firstChangedIndex = sortedChanges[changeIndex].index;\n for (let index = firstChangedIndex; index < cache.length; index++) {\n const entry = cache[index];\n entry.offset += cumulativeDiff;\n\n while (changeIndex < sortedChanges.length && sortedChanges[changeIndex].index === index) {\n const height = sortedChanges[changeIndex].height;\n const heightDiff = height - entry.height;\n entry.measured = true;\n if (heightDiff !== 0) {\n entry.height = height;\n cumulativeDiff += heightDiff;\n }\n changeIndex++;\n }\n }\n}\n\n/**\n * Get total content height from position cache.\n *\n * @param cache - Position cache\n * @returns Total height in pixels\n */\nexport function getTotalHeight(cache: RowPosition[]): number {\n if (cache.length === 0) return 0;\n const last = cache[cache.length - 1];\n return last.offset + last.height;\n}\n\n// #endregion\n\n// #region Binary Search\n\n/**\n * Find the row index at a given scroll offset using binary search.\n * Returns the index of the row that contains the given pixel offset.\n *\n * @param cache - Position cache\n * @param targetOffset - Scroll offset in pixels\n * @returns Row index at that offset, or -1 if cache is empty\n */\nexport function getRowIndexAtOffset(cache: RowPosition[], targetOffset: number): number {\n if (cache.length === 0) return -1;\n if (targetOffset <= 0) return 0;\n\n let low = 0;\n let high = cache.length - 1;\n\n while (low <= high) {\n const mid = Math.floor((low + high) / 2);\n const entry = cache[mid];\n const entryEnd = entry.offset + entry.height;\n\n if (targetOffset < entry.offset) {\n high = mid - 1;\n } else if (targetOffset >= entryEnd) {\n low = mid + 1;\n } else {\n // targetOffset is within this row\n return mid;\n }\n }\n\n // Return the closest row (low will be just past the target)\n return Math.max(0, Math.min(low, cache.length - 1));\n}\n\n// #endregion\n\n// #region Statistics\n\n/**\n * Calculate the average measured height.\n * Used for estimating unmeasured rows.\n *\n * @param cache - Position cache\n * @param defaultHeight - Default height to use if no measurements\n * @returns Average measured height\n */\nexport function calculateAverageHeight(cache: RowPosition[], defaultHeight: number): number {\n let totalHeight = 0;\n let measuredCount = 0;\n\n for (const entry of cache) {\n if (entry.measured) {\n totalHeight += entry.height;\n measuredCount++;\n }\n }\n\n return measuredCount > 0 ? totalHeight / measuredCount : defaultHeight;\n}\n\n/**\n * Count how many rows have been measured.\n *\n * @param cache - Position cache\n * @returns Number of measured rows\n */\nexport function countMeasuredRows(cache: RowPosition[]): number {\n let count = 0;\n for (const entry of cache) {\n if (entry.measured) count++;\n }\n return count;\n}\n\n// #endregion\n// #region Row Measurement\n\n/**\n * Result of measuring rendered row heights.\n */\nexport interface RowMeasurementResult {\n /** Whether any heights changed */\n hasChanges: boolean;\n /** Updated measured row count */\n measuredCount: number;\n /** Updated average height */\n averageHeight: number;\n}\n\n/**\n * Context for measuring rendered rows.\n */\nexport interface RowMeasurementContext<T> {\n /** Position cache to update */\n positionCache: RowPosition[];\n /** Height cache for persistence */\n heightCache: HeightCache;\n /** Row data array */\n rows: T[];\n /** Default row height */\n defaultHeight: number;\n /** Start index of rendered window */\n start: number;\n /** End index of rendered window (exclusive) */\n end: number;\n /** Function to get plugin height for a row */\n getPluginHeight?: (row: T, index: number) => number | undefined;\n /** Function to get row ID for height cache keying */\n getRowId?: (row: T) => string | number;\n}\n\n/**\n * Measure rendered row heights from DOM elements and update caches.\n * Returns measurement statistics for updating virtualization state.\n *\n * @param context - Measurement context with all dependencies\n * @param rowElements - NodeList of rendered row elements\n * @returns Measurement result with flags and statistics\n */\nexport function measureRenderedRowHeights<T>(\n context: RowMeasurementContext<T>,\n rowElements: NodeListOf<Element>,\n): RowMeasurementResult {\n const { positionCache, heightCache, rows, start, end, getPluginHeight, getRowId } = context;\n\n let hasChanges = false;\n const heightChanges: RowHeightChange[] = [];\n\n rowElements.forEach((rowEl) => {\n const rowIndexStr = (rowEl as HTMLElement).dataset.rowIndex;\n if (!rowIndexStr) return;\n\n const rowIndex = parseInt(rowIndexStr, 10);\n if (rowIndex < start || rowIndex >= end || rowIndex >= rows.length) return;\n\n const row = rows[rowIndex];\n\n // Check if a plugin provides the height for this row\n const pluginHeight = getPluginHeight?.(row, rowIndex);\n\n if (pluginHeight !== undefined) {\n // Plugin provides height - use it for position cache\n const currentEntry = positionCache[rowIndex];\n if (!currentEntry.measured || Math.abs(currentEntry.height - pluginHeight) > 1) {\n heightChanges.push({ index: rowIndex, height: pluginHeight });\n hasChanges = true;\n }\n return; // Don't measure DOM for plugin-managed rows\n }\n\n // No plugin height - use DOM measurement\n const measuredHeight = (rowEl as HTMLElement).offsetHeight;\n\n if (measuredHeight > 0) {\n const currentEntry = positionCache[rowIndex];\n\n // Only update if height differs significantly (> 1px to avoid oscillation)\n if (!currentEntry.measured || Math.abs(currentEntry.height - measuredHeight) > 1) {\n heightChanges.push({ index: rowIndex, height: measuredHeight });\n setCachedHeight(heightCache, row, measuredHeight, getRowId);\n hasChanges = true;\n }\n }\n });\n\n updateRowHeights(positionCache, heightChanges);\n\n // Recompute stats\n const measuredCount = hasChanges ? countMeasuredRows(positionCache) : 0;\n const averageHeight = hasChanges ? calculateAverageHeight(positionCache, context.defaultHeight) : 0;\n\n return { hasChanges, measuredCount, averageHeight };\n}\n\n/**\n * Compute measurement statistics for a position cache, excluding plugin-managed rows.\n * Used after rebuilding position cache to get accurate averages for non-plugin rows.\n *\n * @param cache - Position cache\n * @param rows - Row data array\n * @param defaultHeight - Default height if no measurements\n * @param getPluginHeight - Function to check if plugin manages row height\n * @returns Object with measured count and average height\n */\nexport function computeAverageExcludingPluginRows<T>(\n cache: RowPosition[],\n rows: T[],\n defaultHeight: number,\n getPluginHeight?: (row: T, index: number) => number | undefined,\n): { measuredCount: number; averageHeight: number } {\n let measuredCount = 0;\n let totalMeasured = 0;\n\n for (let i = 0; i < cache.length; i++) {\n const entry = cache[i];\n if (entry.measured) {\n // Only include in average if plugin doesn't provide height for this row\n const pluginHeight = getPluginHeight?.(rows[i], i);\n if (pluginHeight === undefined) {\n totalMeasured += entry.height;\n measuredCount++;\n }\n }\n }\n\n return {\n measuredCount,\n averageHeight: measuredCount > 0 ? totalMeasured / measuredCount : defaultHeight,\n };\n}\n\n// #endregion\n\n// #region Scroll Mapping (max element-height cap)\n\n/**\n * Maximum spacer element height (px) before browsers cap the rendered height.\n *\n * Chromium's hard cap is `2^25 = 33,554,432` px; Firefox/Safari are similar or larger.\n * We pick `33,500,000` — ~54 KB of headroom under Chromium's cap so that floating-point\n * drift in the fractional mapping math (`scrollTop * rMax / sMax`) cannot push a computed\n * spacer position past the engine cap and silently truncate.\n *\n * For datasets where `totalRows * rowHeight` exceeds this value, the faux-vscroll\n * spacer would silently truncate, making the tail of the dataset unreachable via\n * the native scrollbar / `Ctrl+End`. The grid switches to fractional scroll mapping\n * above this threshold — see {@link computeScrollMapping}.\n *\n * @category Plugin Development\n * @since 2.13.0\n */\nexport const MAX_ELEMENT_HEIGHT_PX = 33_500_000;\n\n/**\n * Mapping between native `scrollTop` (clamped spacer space) and \"virtual\" scroll\n * position (raw row-content space). See {@link computeScrollMapping}.\n *\n * @category Plugin Development\n * @since 2.13.0\n */\nexport interface ScrollMapping {\n /** Raw row-content height in pixels (totalRows * rowHeight or sum of variable heights). */\n rawContentHeight: number;\n /** Effective spacer DOM height — `min(rawContentHeight, maxSpacerHeight)`. */\n spacerHeight: number;\n /** Viewport height used for computing scrollable extents. */\n viewportHeight: number;\n /** True when `rawContentHeight > maxSpacerHeight` — fractional mapping is active. */\n capped: boolean;\n}\n\n/**\n * Compute a mapping between the spacer's native scrollTop and the virtual\n * row-content scroll offset.\n *\n * For datasets within `maxSpacerHeight`, mapping is identity (no transform).\n * Above the cap, the spacer is clamped and `scrollTop` units no longer equal\n * row-content units — call {@link toVirtualScrollTop} / {@link fromVirtualScrollTop}\n * to translate between the two coordinate spaces.\n *\n * @category Plugin Development\n * @since 2.13.0\n */\nexport function computeScrollMapping(\n rawContentHeight: number,\n viewportHeight: number,\n maxSpacerHeight = MAX_ELEMENT_HEIGHT_PX,\n): ScrollMapping {\n const spacerHeight = Math.min(rawContentHeight, maxSpacerHeight);\n return {\n rawContentHeight,\n spacerHeight,\n viewportHeight,\n capped: rawContentHeight > maxSpacerHeight,\n };\n}\n\n/**\n * Translate a native `scrollTop` (spacer space) into a virtual scroll offset\n * in raw row-content space. Identity when the mapping is not capped.\n *\n * The result is clamped to `[0, rawContentHeight - viewportHeight]` so callers\n * never index past the dataset, even when the spacer's actual scrollable extent\n * slightly exceeds `spacerHeight - viewportHeight` (e.g. when a horizontal scrollbar\n * adds bottom padding to the faux spacer).\n *\n * @category Plugin Development\n * @since 2.13.0\n */\nexport function toVirtualScrollTop(scrollTop: number, mapping: ScrollMapping): number {\n if (!mapping.capped) return scrollTop;\n const sMax = mapping.spacerHeight - mapping.viewportHeight;\n const rMax = mapping.rawContentHeight - mapping.viewportHeight;\n if (sMax <= 0 || rMax <= 0) return scrollTop;\n const virtual = (scrollTop * rMax) / sMax;\n if (virtual < 0) return 0;\n if (virtual > rMax) return rMax;\n return virtual;\n}\n\n/**\n * Translate a virtual scroll offset (raw row-content space) into a native\n * `scrollTop` value (spacer space). Identity when the mapping is not capped.\n *\n * @category Plugin Development\n * @since 2.13.0\n */\nexport function fromVirtualScrollTop(virtualTop: number, mapping: ScrollMapping): number {\n if (!mapping.capped) return virtualTop;\n const sMax = mapping.spacerHeight - mapping.viewportHeight;\n const rMax = mapping.rawContentHeight - mapping.viewportHeight;\n if (sMax <= 0 || rMax <= 0) return virtualTop;\n return (virtualTop * sMax) / rMax;\n}\n\n/**\n * Identity scroll mapping (no cap applied). Useful as a default in places that\n * don't yet know the dataset extents. `viewportHeight` is `0` until the first\n * `calculateTotalSpacerHeight` call populates the real extents — safe because\n * `capped: false` short-circuits both translation helpers before they read it.\n *\n * @category Plugin Development\n * @since 2.13.0\n */\nexport function createIdentityScrollMapping(): ScrollMapping {\n return { rawContentHeight: 0, spacerHeight: 0, viewportHeight: 0, capped: false };\n}\n\n// #endregion\n\n// #region Scroll Transform\n\n/**\n * Sub-pixel `translateY` for the `.rows` element at a given native scroll position.\n *\n * Bypass mode (all rows rendered) tracks the raw scroll offset directly. Virtualized mode\n * offsets only the remainder past the window's first row, and rounds that start down to an\n * even index so DOM `nth-child(even)` zebra parity keeps matching data-row parity.\n *\n * @param v Virtual state (structural subset of `VirtualState`).\n * @param rawScrollTop Native `scrollTop` of the faux scrollbar (clamped spacer space).\n */\nexport function computeRowsTranslateY(\n v: {\n rowHeight: number;\n bypassThreshold: number;\n variableHeights: boolean;\n positionCache: RowPosition[] | null;\n scrollMapping: ScrollMapping;\n },\n totalRows: number,\n rawScrollTop: number,\n): number {\n if (totalRows <= v.bypassThreshold) return -rawScrollTop;\n\n const currentScrollTop = toVirtualScrollTop(rawScrollTop, v.scrollMapping);\n const cache = v.positionCache;\n let startRowOffset: number;\n\n if (v.variableHeights && cache && cache.length > 0) {\n let rawStart = getRowIndexAtOffset(cache, currentScrollTop);\n if (rawStart === -1) rawStart = 0;\n const evenAlignedStart = rawStart - (rawStart % 2);\n startRowOffset = cache[evenAlignedStart]?.offset ?? evenAlignedStart * v.rowHeight;\n } else {\n const rawStart = Math.floor(currentScrollTop / v.rowHeight);\n startRowOffset = (rawStart - (rawStart % 2)) * v.rowHeight;\n }\n\n return -(currentScrollTop - startRowOffset);\n}\n\n// #endregion\n\n// #region Fixed-Height Virtual Window\n\n/**\n * Result of computing a virtual window for fixed-height rows.\n * Used by plugins like FilteringPlugin for virtualized dropdowns.\n */\nexport interface VirtualWindow {\n /** First row index to render (inclusive) */\n start: number;\n /** Last row index to render (exclusive) */\n end: number;\n /** Pixel offset to apply to the rows container (translateY) */\n offsetY: number;\n /**\n * Effective spacer DOM height — use this for the scrollable spacer element.\n * Equal to `min(totalRows * rowHeight, maxSpacerHeight)`. Above the cap this\n * is clamped; use {@link VirtualWindow.rawContentHeight} for the un-clamped value.\n */\n totalHeight: number;\n /**\n * Raw, un-clamped row-content height (`totalRows * rowHeight`). Equal to\n * `totalHeight` when the dataset is below `maxSpacerHeight`.\n *\n * @since 2.13.0\n */\n rawContentHeight: number;\n}\n\n/** Parameters for computing the virtual window */\nexport interface VirtualWindowParams {\n /** Total number of rows */\n totalRows: number;\n /** Height of the viewport in pixels */\n viewportHeight: number;\n /** Current scroll top position */\n scrollTop: number;\n /** Height of each row in pixels */\n rowHeight: number;\n /** Number of extra rows to render above/below viewport */\n overscan: number;\n /**\n * Optional cap on the spacer's DOM height. When `totalRows * rowHeight` exceeds\n * this value, `scrollTop` is treated as a spacer-space position and translated\n * to virtual row-content space via fractional mapping. Defaults to {@link MAX_ELEMENT_HEIGHT_PX}.\n * Pass `Infinity` to disable.\n *\n * @since 2.13.0\n */\n maxSpacerHeight?: number;\n}\n\n/**\n * Compute the virtual row window based on scroll position and viewport.\n * Simple fixed-height implementation for plugins needing basic virtualization.\n *\n * For datasets where `totalRows * rowHeight > maxSpacerHeight`, scroll position\n * is mapped fractionally so the entire dataset stays reachable past the browser's\n * single-element height cap (Chromium ~33.5M px). See {@link computeScrollMapping}.\n *\n * @param params - Parameters for computing the window\n * @returns VirtualWindow with start/end indices and transforms\n */\nexport function computeVirtualWindow(params: VirtualWindowParams): VirtualWindow {\n const { totalRows, viewportHeight, scrollTop, rowHeight, overscan, maxSpacerHeight = MAX_ELEMENT_HEIGHT_PX } = params;\n\n const visibleRows = Math.ceil(viewportHeight / rowHeight);\n const rawContentHeight = totalRows * rowHeight;\n const mapping = computeScrollMapping(rawContentHeight, viewportHeight, maxSpacerHeight);\n const virtualScrollTop = toVirtualScrollTop(scrollTop, mapping);\n\n // Render overscan rows in each direction (total window = visible + 2*overscan)\n let start = Math.floor(virtualScrollTop / rowHeight) - overscan;\n if (start < 0) start = 0;\n\n let end = start + visibleRows + overscan * 2;\n if (end > totalRows) end = totalRows;\n\n // Ensure start is adjusted if we hit the end\n if (end === totalRows && start > 0) {\n start = Math.max(0, end - visibleRows - overscan * 2);\n }\n\n return {\n start,\n end,\n offsetY: start * rowHeight,\n totalHeight: mapping.spacerHeight,\n rawContentHeight,\n };\n}\n\n/**\n * Determine if virtualization should be bypassed for small datasets.\n * When there are very few items, the overhead of virtualization isn't worth it.\n *\n * @param totalRows - Total number of items\n * @param threshold - Bypass threshold (skip virtualization if totalRows <= threshold)\n * @returns True if virtualization should be bypassed\n */\nexport function shouldBypassVirtualization(totalRows: number, threshold: number): boolean {\n return totalRows <= threshold;\n}\n\n// #endregion\n","/**\n * Central keyboard handler attached to the host element. Manages navigation, paging,\n * and edit lifecycle triggers while respecting active form field interactions.\n */\nimport { FOCUSABLE_EDITOR_SELECTOR, GridClasses } from '../constants';\nimport type { GridHost } from '../types';\nimport { clearCellFocus, isRTL } from './utils';\nimport { readCellField } from './value-accessor';\nimport { fromVirtualScrollTop, getRowIndexAtOffset, toVirtualScrollTop } from './virtualization';\n\n/** Commit active row edit if the editing plugin provides this method. */\nfunction tryCommitEdit(grid: GridHost): void {\n if (typeof grid.commitActiveRowEdit === 'function') grid.commitActiveRowEdit();\n}\n\nfunction isFormField(el: HTMLElement | null): boolean {\n if (!el) return false;\n const tag = el.tagName;\n if (tag === 'INPUT' || tag === 'SELECT' || tag === 'TEXTAREA') return true;\n if (el.isContentEditable) return true;\n return false;\n}\n\n/** True while a row-scoped edit session is active. */\nfunction isRowEditing(grid: GridHost): boolean {\n return grid._activeEditRows !== undefined && grid._activeEditRows !== -1;\n}\n\n// #region Keyboard Handler\n/**\n * Guard chain deciding whether this keydown belongs to something other than\n * cell navigation and must be left alone.\n */\nfunction shouldIgnoreKeyDown(grid: GridHost, e: KeyboardEvent, target: HTMLElement | null): boolean {\n // Keyboard events whose target lives inside the grid host but OUTSIDE the\n // rows body (toolpanel inputs/buttons, shell-header controls, light-DOM\n // children of <tbw-grid>) own their own keyboard interactions. Treating\n // ArrowUp/ArrowDown/Enter on such a target as cell navigation steals the\n // user's actual interaction target on the very next render cycle.\n // Cells and the grid host itself remain valid navigation targets.\n if (target && target !== grid && !target.closest?.('.rows-body')) return true;\n\n if (isFormField(target)) {\n switch (e.key) {\n // Home/End and horizontal arrows navigate within the text input; Enter\n // and Escape are handled by the input's own handlers first.\n case 'Home':\n case 'End':\n case 'ArrowLeft':\n case 'ArrowRight':\n case 'Enter':\n case 'Escape':\n return true;\n case 'ArrowUp':\n case 'ArrowDown':\n // Number spinners own the vertical arrows; other fields fall through.\n if ((target as HTMLInputElement).tagName === 'INPUT' && (target as HTMLInputElement).type === 'number') {\n return true;\n }\n break;\n }\n }\n\n const colType = grid._visibleColumns[grid._focusCol]?.type;\n return isRowEditing(grid) && colType === 'select' && (e.key === 'ArrowDown' || e.key === 'ArrowUp');\n}\n\n/** Tab / Shift+Tab: advance one cell, wrapping to the next/previous row. */\nfunction navigateTab(grid: GridHost, e: KeyboardEvent, maxRow: number, maxCol: number): void {\n e.preventDefault();\n if (!e.shiftKey) {\n if (grid._focusCol < maxCol) {\n grid._focusCol += 1;\n return;\n }\n tryCommitEdit(grid);\n if (grid._focusRow < maxRow) {\n grid._focusRow += 1;\n grid._focusCol = 0;\n }\n return;\n }\n if (grid._focusCol > 0) {\n grid._focusCol -= 1;\n } else if (grid._focusRow > 0) {\n if (grid._activeEditRows === grid._focusRow) tryCommitEdit(grid);\n grid._focusRow -= 1;\n grid._focusCol = maxCol;\n }\n}\n\n/**\n * ArrowLeft/ArrowRight. `towardEnd` is the LOGICAL direction (ArrowRight); in\n * RTL the physical arrow maps to the opposite column index.\n */\nfunction navigateHorizontal(grid: GridHost, e: KeyboardEvent, maxCol: number, towardEnd: boolean): void {\n const forward = isRTL(grid) ? !towardEnd : towardEnd;\n grid._focusCol = forward ? Math.min(maxCol, grid._focusCol + 1) : Math.max(0, grid._focusCol - 1);\n e.preventDefault();\n}\n\n/** Commit any active row edit, then focus the first/last row (column unchanged). */\nfunction jumpToRowEdge(grid: GridHost, maxRow: number, toEnd: boolean): void {\n if (isRowEditing(grid)) tryCommitEdit(grid);\n grid._focusRow = toEnd ? maxRow : 0;\n}\n\n/** ArrowUp/ArrowDown: one row, or the first/last row with Ctrl/Cmd. */\nfunction navigateVertical(grid: GridHost, e: KeyboardEvent, maxRow: number, down: boolean): void {\n if (e.ctrlKey || e.metaKey) jumpToRowEdge(grid, maxRow, down);\n else {\n if (isRowEditing(grid)) tryCommitEdit(grid);\n grid._focusRow = down ? Math.min(maxRow, grid._focusRow + 1) : Math.max(0, grid._focusRow - 1);\n }\n e.preventDefault();\n}\n\n/**\n * PageUp/PageDown target: one viewport of rows (less any overlay band), so an\n * unconstrained grid pages straight to its edge. 20 rows until layout exists.\n */\nfunction pageTargetRow(grid: GridHost, maxRow: number, down: boolean): number {\n const from = grid._focusRow;\n const virt = grid._virtualization;\n const viewportH = virt?.viewportEl?.clientHeight ?? virt?.container?.clientHeight ?? 0;\n const offsets = grid._getVerticalScrollOffsets?.(from);\n const usable = viewportH - Math.max(0, offsets?.top ?? 0) - Math.max(0, offsets?.bottom ?? 0);\n const pc = virt?.variableHeights ? virt.positionCache : null;\n let target: number;\n if (usable > 0 && pc?.[from]) {\n target = getRowIndexAtOffset(pc, pc[from].offset + (down ? usable : -usable));\n // A row taller than the viewport would otherwise pin focus in place.\n if (target === from) target = from + (down ? 1 : -1);\n } else {\n const rowH = virt?.rowHeight ?? 0;\n const step = usable > 0 && rowH > 0 ? Math.max(1, Math.floor(usable / rowH)) : 20;\n target = from + (down ? step : -step);\n }\n return Math.max(0, Math.min(maxRow, target));\n}\n\n/** Home/End (plus Ctrl/Cmd variants which also jump to the first/last row). */\nfunction navigateRowEdge(grid: GridHost, e: KeyboardEvent, maxRow: number, maxCol: number, toEnd: boolean): void {\n if (e.ctrlKey || e.metaKey) jumpToRowEdge(grid, maxRow, toEnd);\n grid._focusCol = toEnd ? maxCol : 0;\n e.preventDefault();\n ensureCellVisible(grid, toEnd ? { forceScrollRight: true } : { forceScrollLeft: true });\n}\n\n/** Dispatch the unified `cell-activate` event. Returns true when cancelled. */\nfunction emitCellActivate(grid: GridHost, e: KeyboardEvent): boolean {\n const rowIndex = grid._focusRow;\n const colIndex = grid._focusCol;\n const column = grid._visibleColumns[colIndex];\n const row = grid._rows[rowIndex];\n const field = column?.field ?? '';\n const activateEvent = new CustomEvent('cell-activate', {\n cancelable: true,\n detail: {\n rowIndex,\n colIndex,\n column,\n field,\n value: field && row ? readCellField(row, field) : undefined,\n row,\n cellEl: grid.querySelector(`[data-row=\"${rowIndex}\"][data-col=\"${colIndex}\"]`) as HTMLElement | undefined,\n trigger: 'keyboard' as const,\n originalEvent: e,\n },\n });\n grid.dispatchEvent(activateEvent);\n return activateEvent.defaultPrevented;\n}\n\nexport function handleGridKeyDown(grid: GridHost, e: KeyboardEvent): void {\n // Dispatch to plugin system first - if any plugin handles it, stop here\n if (grid._dispatchKeyDown?.(e)) return;\n\n const path = e.composedPath?.() ?? [];\n const target = (path.length ? path[0] : e.target) as HTMLElement | null;\n if (shouldIgnoreKeyDown(grid, e, target)) return;\n\n const maxRow = grid._rows.length - 1;\n const maxCol = grid._visibleColumns.length - 1;\n\n switch (e.key) {\n case 'Tab':\n navigateTab(grid, e, maxRow, maxCol);\n break;\n case 'ArrowDown':\n navigateVertical(grid, e, maxRow, true);\n break;\n case 'ArrowUp':\n navigateVertical(grid, e, maxRow, false);\n break;\n case 'ArrowRight':\n navigateHorizontal(grid, e, maxCol, true);\n break;\n case 'ArrowLeft':\n navigateHorizontal(grid, e, maxCol, false);\n break;\n case 'Home':\n navigateRowEdge(grid, e, maxRow, maxCol, false);\n return;\n case 'End':\n navigateRowEdge(grid, e, maxRow, maxCol, true);\n return;\n case 'PageDown':\n grid._focusRow = pageTargetRow(grid, maxRow, true);\n e.preventDefault();\n break;\n case 'PageUp':\n grid._focusRow = pageTargetRow(grid, maxRow, false);\n e.preventDefault();\n break;\n // NOTE: Enter is normally handled by EditingPlugin. If no plugin handled\n // it, dispatch the unified cell-activate event for custom handling.\n case 'Enter':\n if (emitCellActivate(grid, e)) {\n e.preventDefault();\n return;\n }\n break;\n default:\n return;\n }\n ensureCellVisible(grid);\n}\n// #endregion\n\n// #region Cell Visibility\n/**\n * Options for ensureCellVisible to control scroll behavior.\n */\ninterface EnsureCellVisibleOptions {\n /** Force scroll to the leftmost position (for Home key) */\n forceScrollLeft?: boolean;\n /** Force scroll to the rightmost position (for End key) */\n forceScrollRight?: boolean;\n /** Force horizontal scroll even in edit mode (for Tab navigation) */\n forceHorizontalScroll?: boolean;\n}\n\n/**\n * Vertical: scroll the virtualized container so the focused row is in view.\n *\n * `y` is in raw row-content space (rowHeight × index). Above the browser height\n * cap (#326), `scrollEl.scrollTop` is in clamped spacer space — the two\n * coordinate systems are not interchangeable, so translate scrollTop into\n * virtual space for the comparison and back into native space when writing.\n * For sub-cap datasets the mapping is identity, so this is a no-op.\n *\n * Plugins that paint over the viewport (sticky rows) report the space they\n * obscure via `_getVerticalScrollOffsets`, so the row lands clear of the\n * overlay instead of underneath it — WCAG 2.2 SC 2.4.11 Focus Not Obscured.\n */\nfunction scrollFocusedRowIntoView(grid: GridHost): void {\n if (!grid._virtualization?.enabled) return;\n const { rowHeight, container, viewportEl, scrollMapping } = grid._virtualization;\n // container is the faux scrollbar element that handles actual scrolling;\n // viewportEl is the visible area element that has the correct height.\n const scrollEl = container as HTMLElement | undefined;\n const visibleHeight = viewportEl?.clientHeight ?? scrollEl?.clientHeight ?? 0;\n if (!scrollEl || visibleHeight <= 0) return;\n\n const offsets = grid._getVerticalScrollOffsets?.(grid._focusRow) ?? { top: 0, bottom: 0 };\n if (offsets.skipScroll) return;\n // A malformed or oversized report must never invert the visible band.\n const top = Math.max(0, offsets.top);\n const bottom = Math.max(0, offsets.bottom);\n const usableHeight = visibleHeight - top - bottom;\n if (usableHeight <= 0) return;\n\n const y = grid._focusRow * rowHeight;\n const virtualScrollTop = scrollMapping ? toVirtualScrollTop(scrollEl.scrollTop, scrollMapping) : scrollEl.scrollTop;\n if (y < virtualScrollTop + top) {\n const target = Math.max(0, y - top);\n scrollEl.scrollTop = scrollMapping ? fromVirtualScrollTop(target, scrollMapping) : target;\n } else if (y + rowHeight > virtualScrollTop + top + usableHeight) {\n const target = Math.max(0, y - usableHeight + rowHeight - top);\n scrollEl.scrollTop = scrollMapping ? fromVirtualScrollTop(target, scrollMapping) : target;\n }\n}\n\n/**\n * Resolve the rendered cell for the focused row/col, or `undefined` when the\n * focused row is outside the current virtual window.\n */\nfunction findFocusedCell(grid: GridHost): { rowEl: HTMLElement | null; cell: HTMLElement } | undefined {\n const rowIndex = grid._focusRow;\n const vStart = grid._virtualization.start ?? 0;\n const vEnd = grid._virtualization.end ?? grid._rows.length;\n if (rowIndex < vStart || rowIndex >= vEnd) return undefined;\n\n const rowEl = grid._bodyEl.querySelectorAll('.data-grid-row')[rowIndex - vStart] as HTMLElement | null;\n // Try exact column match first, then query by data-col, then fall back to the\n // first cell (full-width group rows).\n let cell = rowEl?.children[grid._focusCol] as HTMLElement | undefined;\n if (!cell || !cell.classList?.contains('cell')) {\n cell = (rowEl?.querySelector(`.cell[data-col=\"${grid._focusCol}\"]`) ?? rowEl?.querySelector('.cell[data-col]')) as\n HTMLElement | undefined;\n }\n return cell ? { rowEl: rowEl ?? null, cell } : undefined;\n}\n\n/** Horizontal: bring the focused cell into the `.tbw-scroll-area` viewport. */\nfunction scrollCellIntoViewHorizontally(\n grid: GridHost,\n rowEl: HTMLElement | null,\n cell: HTMLElement,\n options?: EnsureCellVisibleOptions,\n): void {\n const scrollArea = grid.querySelector('.tbw-scroll-area') as HTMLElement | null;\n if (!scrollArea) return;\n\n // Home/End always scroll to the edge.\n if (options?.forceScrollLeft) {\n scrollArea.scrollLeft = 0;\n return;\n }\n if (options?.forceScrollRight) {\n scrollArea.scrollLeft = scrollArea.scrollWidth - scrollArea.clientWidth;\n return;\n }\n\n // Plugins (e.g. pinned columns) report how much of the scroll area they\n // obscure, and whether the focused cell should skip scrolling entirely\n // (pinned cells are always visible).\n const offsets = grid._getHorizontalScrollOffsets?.(rowEl ?? undefined, cell) ?? { left: 0, right: 0 };\n if (offsets.skipScroll) return;\n\n const cellRect = cell.getBoundingClientRect();\n const scrollAreaRect = scrollArea.getBoundingClientRect();\n const cellLeft = cellRect.left - scrollAreaRect.left + scrollArea.scrollLeft;\n const cellRight = cellLeft + cellRect.width;\n const visibleLeft = scrollArea.scrollLeft + offsets.left;\n const visibleRight = scrollArea.scrollLeft + scrollArea.clientWidth - offsets.right;\n if (cellLeft < visibleLeft) {\n scrollArea.scrollLeft = cellLeft - offsets.left;\n } else if (cellRight > visibleRight) {\n scrollArea.scrollLeft = cellRight - scrollArea.clientWidth + offsets.right;\n }\n}\n\n/** Move real DOM focus to the editor, the cell, or the grid host. */\nfunction applyFocusTarget(grid: GridHost, cell: HTMLElement, isEditing: boolean): void {\n if (isEditing && cell.classList.contains(GridClasses.EDITING)) {\n // Editing cell: focus the editor input inside it\n const focusTarget = cell.querySelector(FOCUSABLE_EDITOR_SELECTOR) as HTMLElement | null;\n if (focusTarget && document.activeElement !== focusTarget) {\n try {\n focusTarget.focus({ preventScroll: true });\n } catch {\n /* empty */\n }\n }\n return;\n }\n\n if (isEditing) {\n // Active edit row but this cell isn't the editing cell — focus it so Tab\n // navigation within the row can attach editors.\n if (cell.contains(document.activeElement)) return;\n if (!cell.hasAttribute('tabindex')) cell.setAttribute('tabindex', '-1');\n try {\n cell.focus({ preventScroll: true });\n } catch {\n /* empty */\n }\n return;\n }\n\n // NOT editing: keep focus on the grid element (tabindex=0) rather than\n // individual cells. In a virtualized grid, cells can be detached by\n // subsequent render cycles (e.g., SelectionPlugin's requestAfterRender\n // → RAF → row recycling). A detached focused cell causes activeElement\n // to revert to <body>, breaking keyboard navigation.\n // Visual focus is managed by the .cell-focus CSS class + data-has-focus.\n //\n // BUT: don't steal focus from a real focused descendant (toolpanel\n // input, shell-header button, registered overlay control). Cell focus\n // is virtual; if the user is actively typing in a non-cell input, we\n // must leave that input alone. We only reclaim focus when nothing\n // meaningful inside the grid currently holds it (active element is\n // outside grid, on the grid host itself, or on a bare cell).\n const active = document.activeElement;\n const meaningful =\n active instanceof HTMLElement &&\n active !== grid &&\n typeof grid.contains === 'function' &&\n grid.contains(active) &&\n !active.classList.contains('cell') &&\n !active.closest('.cell');\n if (!meaningful && active !== grid) {\n grid.focus({ preventScroll: true });\n }\n}\n\n/**\n * Scroll the viewport (virtualized or static) so the focused cell's row is visible\n * and apply visual focus styling / tabindex management.\n */\nexport function ensureCellVisible(grid: GridHost, options?: EnsureCellVisibleOptions): void {\n scrollFocusedRowIntoView(grid);\n\n const isEditing = isRowEditing(grid) || !!grid._isGridEditMode;\n // Skip refreshVirtualWindow when in edit mode to avoid wiping editors\n if (!isEditing) grid.refreshVirtualWindow(false);\n\n clearCellFocus(grid._bodyEl);\n // Clear previous aria-selected markers\n Array.from(grid._bodyEl.querySelectorAll('[aria-selected=\"true\"]')).forEach((el) => {\n el.setAttribute('aria-selected', 'false');\n });\n\n const focused = findFocusedCell(grid);\n if (!focused) return;\n const { rowEl, cell } = focused;\n\n cell.classList.add('cell-focus');\n cell.setAttribute('aria-selected', 'true');\n\n // Skip horizontal scrolling in edit mode to prevent scroll jumps when editors\n // are created — unless forceHorizontalScroll is set (Tab while editing).\n if (!isEditing || options?.forceHorizontalScroll) {\n scrollCellIntoViewHorizontally(grid, rowEl, cell, options);\n }\n\n applyFocusTarget(grid, cell, isEditing);\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, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''');\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 * 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 * 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 * Factory for a default instance of the dependency. When the dependency is\n * missing, the grid attaches the instance this returns (once per grid,\n * before the depending plugin) instead of reporting an error, so consumers\n * never have to add it themselves. An explicitly configured instance always\n * wins.\n *\n * Import the dependency's plugin module statically so it ships with the\n * depending plugin's module graph — never with `import()`.\n *\n * @example provide: () => new HierarchyPlugin()\n * @since 3.9.0\n */\n provide?: () => BaseGridPlugin;\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 * Responsive Plugin CSS\n *\n * Card layout styles for narrow viewports.\n * Applied when [data-responsive] attribute is present on the grid.\n *\n * IMPORTANT: This CSS intentionally preserves the grid's virtualization system.\n * The row height observer will detect the new card height automatically and\n * recalculate the virtual window. Do NOT break virtualization by:\n * - Changing .rows-viewport overflow\n * - Changing .rows position\n * - Hiding .faux-vscroll\n */\n\n/* ============================================\n Animation / Transition Support\n ============================================ */\n\n/* View transition path: the compositor cross-fades the whole grid (or, with\n `animation: 'morph-rows' | 'morph-cells'`, each row or cell) between the table\n and card layouts. While this is active the plugin withholds\n `data-responsive-animate`, so the keyframe fade below never runs alongside it. */\ntbw-grid[data-responsive-transition]::view-transition-group(*) {\n animation-duration: var(--tbw-responsive-duration, 200ms);\n}\n\n/* `morph-cells` names every cell individually. One `view-transition-class` keeps\n that a single style rule instead of one rule per generated name — the class\n does NOT capture anything on its own, the plugin still authors the names.\n Cells travel a real distance between the two layouts, so they get an\n ease-in-out curve rather than the root's cross-fade. */\ntbw-grid[data-responsive-transition] .data-grid-row > .cell {\n view-transition-class: tbw-responsive-cell;\n}\n\ntbw-grid[data-responsive-transition]::view-transition-group(.tbw-responsive-cell) {\n animation-timing-function: ease-in-out;\n}\n\n/* A cell's box grows from one column to a full card row, and the UA stretches\n both snapshots to fill that box — so the text visibly zooms while it travels.\n Drawing them at natural size keeps the glyphs the size they actually are at\n both ends; the group still animates the box, so the cell still moves.\n Anchoring right lands the value on the card's right-aligned value slot rather\n than on the `Header:` label, and lets the label reveal as the box widens. */\ntbw-grid[data-responsive-transition]::view-transition-old(.tbw-responsive-cell),\ntbw-grid[data-responsive-transition]::view-transition-new(.tbw-responsive-cell) {\n width: 100%;\n height: 100%;\n object-fit: none;\n object-position: right center;\n}\n\n/* The old bitmap holds its value left-aligned in a column, the new one holds it\n right-aligned in a card — so the two can never line up exactly, and the gap\n varies with the text length. Front-load the cross-fade instead: the swap is\n over while the boxes are still visibly apart, and the rest of the travel runs\n on a single crisp copy. */\ntbw-grid[data-responsive-transition]::view-transition-old(.tbw-responsive-cell),\ntbw-grid[data-responsive-transition]::view-transition-new(.tbw-responsive-cell) {\n animation-duration: calc(var(--tbw-responsive-duration, 200ms) * 0.35);\n}\n\n/* Base transition setup for animated mode switching */\ntbw-grid[data-responsive-animate] {\n .data-grid-row,\n .data-grid-row > .cell {\n transition:\n opacity var(--tbw-responsive-duration, 200ms) ease-out,\n transform var(--tbw-responsive-duration, 200ms) ease-out;\n }\n}\n\n/* Fade-in when entering responsive mode */\ntbw-grid[data-responsive][data-responsive-animate] {\n .data-grid-row {\n animation: responsive-card-enter var(--tbw-responsive-duration, 200ms) ease-out;\n }\n}\n\n@keyframes responsive-card-enter {\n from {\n opacity: 0;\n transform: translateY(-10px);\n }\n to {\n opacity: 1;\n transform: translateY(0);\n }\n}\n\n/* ============================================\n Responsive Card Layout\n ============================================ */\n\ntbw-grid[data-responsive] {\n /* Hide entire header section in card mode */\n .header {\n display: none !important;\n }\n\n /* Hide footer row in card mode (aggregations don't make sense in cards) */\n .footer-row {\n display: none;\n }\n\n /* Override horizontal scroll - single column flow */\n .tbw-scroll-area {\n overflow-x: hidden;\n min-width: 0 !important;\n }\n\n /* Override rows-body-wrapper to allow natural flow */\n .rows-body-wrapper {\n min-width: 0 !important;\n }\n\n /* Convert row from CSS Grid to block layout */\n .data-grid-row:not(.group-row) {\n display: block !important;\n grid-template-columns: none !important;\n padding: var(--tbw-cell-padding);\n padding-inline-start: var(--tbw-spacing-xl);\n border-bottom: 1px solid var(--tbw-color-border);\n /* Disable containment so row can grow to fit stacked cells */\n contain: none !important;\n content-visibility: visible !important;\n background: var(--tbw-color-bg);\n position: relative;\n\n &:nth-child(even) {\n background: var(--tbw-color-row-alt);\n }\n\n &:hover {\n background: var(--tbw-color-row-hover);\n }\n\n &[aria-selected='true'] {\n background: var(--tbw-color-selection);\n\n /* Selection indicator bar */\n &::before {\n content: '';\n position: absolute;\n inset-inline-start: 0;\n top: 0;\n bottom: 0;\n width: 4px;\n background: var(--tbw-color-accent);\n }\n }\n }\n\n /* The default stacked-cell card grows to fit its cells. A custom `cardRenderer`\n row sets its own height from `cardRowHeight`, so it must not be forced open. */\n .data-grid-row:not(.group-row, .responsive-card) {\n min-height: auto !important;\n height: auto !important;\n }\n\n /* Stack cells vertically as label: value pairs */\n .data-grid-row:not(.group-row) > .cell {\n display: flex !important;\n justify-content: space-between;\n align-items: center;\n padding: var(--tbw-spacing-xs) var(--tbw-spacing-md);\n width: 100% !important;\n min-width: 0 !important;\n min-height: auto !important;\n height: auto !important;\n line-height: 1.5 !important;\n position: static !important;\n left: auto !important;\n right: auto !important;\n border: none !important;\n border-bottom: none !important;\n border-inline-end: none !important;\n background: transparent !important;\n white-space: normal !important;\n overflow: visible !important;\n\n /* Header label from data-header attribute */\n &::before {\n content: attr(data-header) ': ';\n font-weight: 600;\n color: var(--tbw-color-header-fg);\n flex-shrink: 0;\n margin-inline-end: var(--tbw-spacing-md);\n min-width: 100px;\n }\n\n /* Value takes remaining space */\n &::after {\n content: none;\n }\n }\n\n /* Hide utility columns (row expander, selection checkbox, etc.) */\n .cell[data-utility] {\n display: none !important;\n }\n}\n\n/* Hidden columns — specificity must beat `.data-grid-row:not(.group-row) > .cell` above */\ntbw-grid[data-responsive] .data-grid-row > .cell[data-responsive-hidden] {\n display: none !important;\n}\n\n/* Value-only columns in responsive mode (hides label, shows value full-width) */\ntbw-grid[data-responsive] .data-grid-row > .cell[data-responsive-value-only] {\n /* Override the header label ::before to hide it */\n &::before {\n display: none !important;\n }\n\n /* Value takes full width, no justify-content needed */\n justify-content: flex-start !important;\n font-weight: 500;\n}\n\n/* Opt-in: hide per-card field labels (the \"Name:\" prefix) when\n `hideHeader: true` is configured. Toggled by ResponsivePlugin. */\ntbw-grid[data-responsive][data-responsive-hide-header] .data-grid-row:not(.group-row) > .cell::before {\n display: none !important;\n}\n\n/* Non-responsive mode: a hidden column collapses its grid track to zero width\n rather than leaving layout. The cell must stay in flow — cells are auto-placed,\n so dropping one would pull every later cell into the wrong track. Padding and\n border are zeroed too, or the cell would floor at ~17px and the track could\n never reach zero. Opacity is deliberately not `!important`: the plugin fades\n the changed cells with WAAPI, and `!important` outranks animations. */\ntbw-grid:not([data-responsive]) .cell[data-responsive-hidden] {\n padding-inline: 0 !important;\n border-inline-width: 0 !important;\n overflow: hidden !important;\n pointer-events: none;\n opacity: 0;\n}\n\n/* `grid-template-columns` is animatable, and a row element survives a render\n (only its cells are rebuilt), so the collapse is a plain transition. Gated on\n the fade window so column resizing stays instant. A track list only\n interpolates pairwise, which holds here — collapsing never changes the number\n of tracks. */\ntbw-grid[data-responsive-column-fade] :is(.header-row, .data-grid-row) {\n transition: grid-template-columns var(--tbw-responsive-duration, 200ms) ease-out;\n}\n\n/* Hide pinned/aggregation rows in responsive mode - they don't make sense in card layout */\ntbw-grid[data-responsive] {\n .tbw-footer,\n .tbw-header-pinned,\n .tbw-pinned-rows,\n .tbw-aggregation-rows {\n display: none !important;\n }\n\n /* Reset min-widths that hold horizontal scroll width */\n .tbw-pinned-rows,\n .tbw-aggregation-rows,\n .tbw-aggregation-row {\n min-width: 0 !important;\n }\n}\n\n/* Custom card renderer styling */\ntbw-grid[data-responsive] .data-grid-row.responsive-card {\n /* Override default cell layout - custom content takes full width */\n display: block !important;\n padding: var(--tbw-cell-padding);\n border-bottom: 1px solid var(--tbw-color-border);\n\n /* Card content should not have cell-based constraints */\n > * {\n width: 100%;\n }\n\n /* Remove default cell ::before headers - custom renderer handles layout */\n .cell::before {\n display: none;\n }\n}\n","/**\n * Responsive Plugin\n *\n * Transforms the grid from tabular layout to a card/list layout when the grid\n * width falls below a configurable breakpoint. This enables grids to work in\n * narrow containers (split-pane UIs, mobile viewports, dashboard widgets).\n *\n * ## Installation\n *\n * ```ts\n * import { ResponsivePlugin } from '@toolbox-web/grid/plugins/responsive';\n *\n * const config: GridConfig = {\n * plugins: [new ResponsivePlugin({ breakpoint: 500 })],\n * };\n * ```\n *\n * ## How It Works\n *\n * 1. ResizeObserver monitors the grid element's width\n * 2. When `width < breakpoint`, adds `data-responsive` attribute to grid\n * 3. CSS transforms cells from horizontal to vertical layout\n * 4. Each cell displays \"Header: Value\" using CSS `::before` pseudo-element\n *\n * @see [Responsive Demo](?path=/story/grid-plugins-responsive--default)\n */\n\nimport { MISSING_BREAKPOINT } from '../../core/internal/diagnostics';\nimport { ensureCellVisible } from '../../core/internal/keyboard';\nimport { evalTemplateString, setSanitizedHTML } from '../../core/internal/sanitize';\nimport { BaseGridPlugin, type GridElement, type PluginManifest, type PluginQuery } from '../../core/plugin/base-plugin';\nimport type { GridHost } from '../../core/types';\nimport styles from './responsive.css?inline';\nimport type {\n BreakpointConfig,\n HiddenColumnConfig,\n ResponsiveAnimation,\n ResponsiveChangeDetail,\n ResponsivePluginConfig,\n} from './types';\n\n/**\n * The subset of the View Transition API this plugin depends on.\n *\n * `Element.startViewTransition()` is not yet in the TypeScript DOM lib, so the\n * shape is declared locally rather than widening the global `Element`.\n */\ntype ViewTransitionStarter = (update: () => Promise<void>) => {\n readonly finished: Promise<void>;\n /** Rejects when the UA skips the transition — routine while dragging a resize. */\n readonly ready?: Promise<void>;\n};\ntype ViewTransitionCapableElement = HTMLElement & { startViewTransition?: ViewTransitionStarter };\n\n/** How finely the layout switch is broken up for the compositor. */\ntype MorphGranularity = 'cells' | 'rows' | 'none';\n\n/** Disambiguates row morph names when several grids transition at once. */\nlet morphInstanceCounter = 0;\n\n/**\n * Above this, per-cell morphing costs more in compositor layers than it buys.\n * Each named cell is 3 animations and 2 snapshot textures, and rows are\n * virtualized — so this bounds the switch by viewport size, not by dataset size.\n */\nconst MAX_MORPH_CELLS = 150;\n\n/** Stands in for a column set that is not currently applied, without allocating. */\nconst EMPTY_FIELD_SET: ReadonlySet<string> = new Set<string>();\n\n/** Tags the column fade so a re-render can cancel its own leftovers and nothing else. */\nconst COLUMN_FADE_ID = 'tbw-responsive-column-fade';\n\n/** Present only while columns are collapsing, so column resizing stays instant. */\nconst COLUMN_FADE_ATTR = 'data-responsive-column-fade';\n\n/**\n * Cancel this plugin's fades on `cell` and nothing else. Cancelling REJECTS\n * `Animation.finished` with an `AbortError`, and nothing awaits it, so without a\n * sink it surfaces as an unhandled rejection.\n */\nfunction cancelColumnFades(cell: HTMLElement): void {\n for (const animation of cell.getAnimations?.() ?? []) {\n if (animation.id !== COLUMN_FADE_ID) continue;\n animation.finished?.catch(() => undefined);\n animation.cancel();\n }\n}\n\n/** Split a grid track list on top-level spaces, keeping `minmax(a, b)` intact. */\nfunction splitTrackList(template: string): string[] {\n const tracks: string[] = [];\n let depth = 0;\n let start = 0;\n for (let i = 0; i < template.length; i++) {\n const char = template[i];\n if (char === '(') depth++;\n else if (char === ')') depth--;\n else if (char === ' ' && depth === 0) {\n if (i > start) tracks.push(template.slice(start, i));\n start = i + 1;\n }\n }\n if (start < template.length) tracks.push(template.slice(start));\n return tracks;\n}\n\n/**\n * The zero-width form of `track`. A track list only interpolates pairwise, so the\n * collapsed track has to keep the same unit family — `1fr` has to become `0fr`,\n * not `0px`. Keywords (`auto`, `max-content`) have no interpolable zero and snap.\n */\nfunction collapsedTrack(track: string): string {\n if (track.startsWith('minmax(')) return track.includes('fr') ? 'minmax(0px, 0fr)' : 'minmax(0px, 0px)';\n if (track.endsWith('fr')) return '0fr';\n if (track.endsWith('%')) return '0%';\n return '0px';\n}\n\n/**\n * Responsive Plugin for tbw-grid\n *\n * Adds automatic card layout mode when the grid width falls below a configurable\n * breakpoint. Perfect for responsive designs, split-pane UIs, and mobile viewports.\n *\n * @template T The row data type\n *\n * @example\n * ```ts\n * // Basic usage - switch to card layout below 500px\n * const config: GridConfig = {\n * plugins: [new ResponsivePlugin({ breakpoint: 500 })],\n * };\n * ```\n *\n * @example\n * ```ts\n * // Hide less important columns in card mode\n * const config: GridConfig = {\n * plugins: [\n * new ResponsivePlugin({\n * breakpoint: 600,\n * hiddenColumns: ['createdAt', 'updatedAt'],\n * }),\n * ],\n * };\n * ```\n *\n * @example\n * ```ts\n * // Custom card renderer for advanced layouts\n * const config: GridConfig = {\n * plugins: [\n * new ResponsivePlugin({\n * breakpoint: 400,\n * cardRenderer: (row) => {\n * const card = document.createElement('div');\n * card.className = 'custom-card';\n * card.innerHTML = `<strong>${row.name}</strong><br>${row.email}`;\n * return card;\n * },\n * }),\n * ],\n * };\n * ```\n * @since 1.1.0\n */\nexport class ResponsivePlugin<T = unknown> extends BaseGridPlugin<ResponsivePluginConfig<T>> {\n readonly name = 'responsive';\n override readonly version = '1.0.0';\n override readonly styles = styles;\n\n /**\n * Plugin manifest declaring queries this plugin handles.\n */\n static override readonly manifest: PluginManifest = {\n queries: [\n {\n type: 'isCardMode',\n description: 'Returns whether the grid is currently in responsive card mode',\n },\n ],\n };\n\n #resizeObserver?: ResizeObserver;\n #isResponsive = false;\n #debounceTimer?: ReturnType<typeof setTimeout>;\n #warnedAboutMissingBreakpoint = false;\n #currentWidth = 0;\n /** Width the last `#checkBreakpoint()` ran against — guards height-only resizes. */\n #lastCheckedWidth?: number;\n /** Timestamp of the last applied layout switch, for the post-switch cooldown. */\n #lastSwitchAt = Number.NEGATIVE_INFINITY;\n /** Unique `view-transition-name` prefixes for this instance's rows and cells. */\n readonly #morphInstance = ++morphInstanceCounter;\n readonly #morphPrefix = `tbw-responsive-row-${this.#morphInstance}-`;\n readonly #cellMorphPrefix = `tbw-responsive-cell-${this.#morphInstance}-`;\n /** Identifies the newest view transition, so superseded ones skip their cleanup. */\n #transitionToken = 0;\n /** Set of column fields to completely hide */\n #hiddenColumnSet: Set<string> = new Set();\n /** The subset of the above that the last render actually hid. */\n #appliedHiddenFields: ReadonlySet<string> = EMPTY_FIELD_SET;\n /** Set of column fields to show value only (no header label) */\n #valueOnlyColumnSet: Set<string> = new Set();\n /** The subset of the above that the last render actually stripped labels from. */\n #appliedValueOnlyFields: ReadonlySet<string> = EMPTY_FIELD_SET;\n /** Fields currently fading out of the table, and the ones fading in. */\n #fadeOutFields: Set<string> = new Set();\n #fadeInFields: Set<string> = new Set();\n /** When the in-flight column fade ends, so a mid-fade re-render can resume it. */\n #fadeEndsAt = 0;\n #fadeTimer?: ReturnType<typeof setTimeout>;\n /** Whether the grid template currently carries this plugin's collapsed tracks. */\n #templateOverridden = false;\n /** Currently active breakpoint, or null if none */\n #activeBreakpoint: BreakpointConfig | null = null;\n /** Sorted breakpoints from largest to smallest */\n #sortedBreakpoints: BreakpointConfig[] = [];\n\n /** Typed internal grid accessor — centralizes the single required cast. */\n get #internalGrid(): GridHost {\n return this.grid;\n }\n\n /**\n * Check if currently in responsive mode.\n * @returns `true` if the grid is in card layout mode\n */\n isResponsive(): boolean {\n return this.#isResponsive;\n }\n\n /**\n * Force responsive mode regardless of width.\n * Useful for testing or manual control.\n * @param enabled - Whether to enable responsive mode\n */\n setResponsive(enabled: boolean): void {\n if (enabled !== this.#isResponsive) {\n this.#isResponsive = enabled;\n this.#commitLayoutSwitch();\n this.emit('responsive-change', {\n isResponsive: enabled,\n width: this.#currentWidth,\n breakpoint: this.config.breakpoint ?? 0,\n } satisfies ResponsiveChangeDetail);\n }\n }\n\n /**\n * Update breakpoint dynamically.\n * @param width - New breakpoint width in pixels\n */\n setBreakpoint(width: number): void {\n this.config.breakpoint = width;\n // The guard keys off width alone, so a config change must invalidate it.\n this.#lastCheckedWidth = undefined;\n this.#checkBreakpoint(this.#currentWidth);\n }\n\n /**\n * Set a custom card renderer.\n * This allows framework adapters to provide template-based renderers at runtime.\n * @param renderer - The card renderer function, or undefined to use default\n */\n setCardRenderer(renderer: ResponsivePluginConfig<T>['cardRenderer']): void {\n this.config.cardRenderer = renderer;\n // If already in responsive mode, trigger a re-render to apply the new renderer\n if (this.#isResponsive) {\n this.requestRender();\n }\n }\n\n /**\n * Get current grid width.\n * @returns Width of the grid element in pixels\n */\n getWidth(): number {\n return this.#currentWidth;\n }\n\n /**\n * Get the currently active breakpoint config (multi-breakpoint mode only).\n * @returns The active BreakpointConfig, or null if no breakpoint is active\n */\n getActiveBreakpoint(): BreakpointConfig | null {\n return this.#activeBreakpoint;\n }\n\n override attach(grid: GridElement): void {\n super.attach(grid);\n\n // Parse light DOM configuration first (may update this.config)\n this.#parseLightDomCard();\n\n // Build hidden column sets from config\n this.#buildHiddenColumnSets(this.config.hiddenColumns);\n\n // Sort breakpoints from largest to smallest for evaluation\n if (this.config.breakpoints?.length) {\n this.#sortedBreakpoints = [...this.config.breakpoints].sort((a, b) => b.maxWidth - a.maxWidth);\n }\n\n this.#syncMotionDuration();\n\n // A `gridConfig` rebuild detaches and re-attaches this same instance, which\n // strips the attributes but keeps the layout state — so re-assert them here or\n // the grid sits in card mode with none of the card CSS until the next resize.\n this.#syncLayoutAttributes();\n\n // Card height is measured from the DOM and outranks the config, so a rebuild\n // carrying a new `cardRowHeight` would keep serving the previous measurement.\n this.#measuredCardHeight = undefined;\n this.#measuredGroupRowHeight = undefined;\n\n // The cards on screen were rendered under the previous config.\n if (this.#isResponsive) this.requestRender();\n\n // Observe the grid element itself (not internal viewport)\n // This captures the container width including when shell panels open/close\n this.#resizeObserver = new ResizeObserver((entries) => {\n this.#onResize(entries[0]?.contentRect.width ?? 0);\n });\n\n this.#resizeObserver.observe(this.gridElement);\n }\n\n /**\n * React to an observed size change.\n *\n * ResizeObserver also fires for HEIGHT changes, and host apps commonly animate\n * a container's height (or load content into it) while the width is already\n * final. A trailing-only debounce is reset by every one of those, delaying the\n * layout switch long past the moment the final width was known. So:\n *\n * 1. Height-only notifications are dropped — they cannot change the outcome.\n * 2. A width change switches on the LEADING edge when no switch happened in\n * the last `debounceMs`, so a settled width applies within one frame.\n * 3. Further width changes inside that window are deferred to a single\n * trailing evaluation, which rate-limits thrash during a resize drag.\n */\n #onResize(width: number): void {\n this.#currentWidth = width;\n\n // Height-only resize — nothing the breakpoint logic reads has changed.\n if (width === this.#lastCheckedWidth) return;\n\n const debounceMs = this.config.debounceMs ?? 100;\n const sinceLastSwitch = performance.now() - this.#lastSwitchAt;\n\n clearTimeout(this.#debounceTimer);\n\n if (sinceLastSwitch >= debounceMs) {\n this.#lastCheckedWidth = width;\n this.#checkBreakpoint(width);\n return;\n }\n\n this.#debounceTimer = setTimeout(() => {\n this.#lastCheckedWidth = this.#currentWidth;\n this.#checkBreakpoint(this.#currentWidth);\n }, debounceMs - sinceLastSwitch);\n }\n\n // #region Light DOM Parsing\n\n /**\n * Parse `<tbw-grid-responsive-card>` elements from the grid's light DOM.\n *\n * Allows declarative configuration:\n * ```html\n * <tbw-grid [rows]=\"data\">\n * <tbw-grid-responsive-card breakpoint=\"500\" card-row-height=\"80\">\n * <div class=\"custom-card\">\n * <strong>{{ row.name }}</strong>\n * <span>{{ row.email }}</span>\n * </div>\n * </tbw-grid-responsive-card>\n * </tbw-grid>\n * ```\n *\n * Attributes:\n * - `breakpoint`: number - Width threshold for responsive mode\n * - `card-row-height`: number | 'auto' - Card height (default: 'auto')\n * - `hidden-columns`: string - Comma-separated fields to hide\n * - `hide-header`: 'true' | 'false' - Hide per-card field labels (default: 'false')\n * - `debounce-ms`: number - Resize debounce delay (default: 100)\n */\n #parseLightDomCard(): void {\n const gridEl = this.gridElement;\n if (!gridEl) return;\n\n const cardEl = gridEl.querySelector('tbw-grid-responsive-card');\n if (!cardEl) return;\n\n // Check if a framework adapter wants to handle this element\n // (e.g., React adapter intercepts for JSX rendering)\n const adapter = this.#internalGrid.__frameworkAdapter;\n if (adapter?.parseResponsiveCardElement) {\n const adapterRenderer = adapter.parseResponsiveCardElement(cardEl);\n if (adapterRenderer) {\n this.config = { ...this.config, cardRenderer: adapterRenderer };\n // Continue to parse attributes even if adapter provides renderer\n }\n }\n\n // Parse attributes for configuration\n const breakpointAttr = cardEl.getAttribute('breakpoint');\n const cardRowHeightAttr = cardEl.getAttribute('card-row-height');\n const hiddenColumnsAttr = cardEl.getAttribute('hidden-columns');\n const hideHeaderAttr = cardEl.getAttribute('hide-header');\n const debounceMsAttr = cardEl.getAttribute('debounce-ms');\n\n const configUpdates: Partial<ResponsivePluginConfig<T>> = {};\n\n if (breakpointAttr !== null) {\n const breakpoint = parseInt(breakpointAttr, 10);\n if (!isNaN(breakpoint)) {\n configUpdates.breakpoint = breakpoint;\n }\n }\n\n if (cardRowHeightAttr !== null) {\n configUpdates.cardRowHeight = cardRowHeightAttr === 'auto' ? 'auto' : parseInt(cardRowHeightAttr, 10);\n }\n\n if (hiddenColumnsAttr !== null) {\n // Parse comma-separated field names\n configUpdates.hiddenColumns = hiddenColumnsAttr\n .split(',')\n .map((s) => s.trim())\n .filter((s) => s.length > 0);\n }\n\n if (hideHeaderAttr !== null) {\n configUpdates.hideHeader = hideHeaderAttr !== 'false';\n }\n\n if (debounceMsAttr !== null) {\n const debounceMs = parseInt(debounceMsAttr, 10);\n if (!isNaN(debounceMs)) {\n configUpdates.debounceMs = debounceMs;\n }\n }\n\n // Get template content from innerHTML (only if no renderer already set)\n const templateHTML = cardEl.innerHTML.trim();\n if (templateHTML && !this.config.cardRenderer && !adapter?.parseResponsiveCardElement) {\n // Create a template-based renderer using the inner HTML\n configUpdates.cardRenderer = (row: T): HTMLElement => {\n // Evaluate template expressions like {{ row.field }}\n const evaluated = evalTemplateString(templateHTML, { value: row, row: row as Record<string, unknown> });\n const container = document.createElement('div');\n container.className = 'tbw-responsive-card-content';\n // Sanitize the result to prevent XSS\n setSanitizedHTML(container, evaluated);\n return container;\n };\n }\n\n // Merge updates into config (light DOM values override constructor config)\n if (Object.keys(configUpdates).length > 0) {\n this.config = { ...this.config, ...configUpdates };\n }\n }\n\n // #endregion\n\n /**\n * Build the hidden and value-only column sets from config.\n */\n #buildHiddenColumnSets(hiddenColumns?: HiddenColumnConfig[]): void {\n this.#hiddenColumnSet = new Set();\n this.#valueOnlyColumnSet.clear();\n\n for (const col of hiddenColumns ?? []) {\n if (typeof col === 'string') {\n this.#hiddenColumnSet.add(col);\n } else if (col.showValue) {\n this.#valueOnlyColumnSet.add(col.field);\n } else {\n this.#hiddenColumnSet.add(col.field);\n }\n }\n\n this.#queueHiddenColumnFades();\n }\n\n /**\n * Whether `hiddenColumns` is in effect right now: card mode drops the column\n * from the card, and in progressive degradation a matched breakpoint drops it\n * from the table. A plain table with no matching breakpoint shows everything.\n */\n get #hiddenColumnsApply(): boolean {\n return this.#sortedBreakpoints.length > 0 ? this.#activeBreakpoint !== null : this.#isResponsive;\n }\n\n /**\n * Queue a fade for every column whose visibility actually changed. The diff is\n * against what the last render hid rather than against the previous config,\n * because `hiddenColumns` is inert in a plain table — fading a column out there\n * would strand it invisible in a track that is never collapsed.\n */\n #queueHiddenColumnFades(): void {\n const applied = this.#hiddenColumnsApply ? this.#hiddenColumnSet : EMPTY_FIELD_SET;\n\n for (const field of this.#appliedHiddenFields) {\n if (!applied.has(field)) this.#markColumnFade(field, false);\n }\n for (const field of applied) {\n if (!this.#appliedHiddenFields.has(field)) this.#markColumnFade(field, true);\n }\n this.#openFadeWindow();\n }\n\n /** Queue `field` for the next fade, dropping any opposite direction still pending for it. */\n #markColumnFade(field: string, out: boolean): void {\n (out ? this.#fadeInFields : this.#fadeOutFields).delete(field);\n (out ? this.#fadeOutFields : this.#fadeInFields).add(field);\n }\n\n /**\n * Start the window during which queued columns animate. The track collapse is a\n * CSS transition on the row, which survives a render, but the opacity fade is\n * not: cells are rebuilt on every render, so a hidden column's cell would be\n * re-inserted visible and transition to hidden all over again — exactly the\n * flash of every hidden column reappearing. Cells are animated explicitly\n * instead, and only the fields that actually changed are queued.\n */\n #openFadeWindow(): void {\n const duration = this.#motionDurationMs;\n if (duration <= 0 || (this.#fadeOutFields.size === 0 && this.#fadeInFields.size === 0)) {\n this.#endFadeWindow();\n return;\n }\n\n this.#fadeEndsAt = performance.now() + duration;\n this.gridElement?.setAttribute(COLUMN_FADE_ATTR, '');\n clearTimeout(this.#fadeTimer);\n this.#fadeTimer = setTimeout(() => this.#endFadeWindow(), duration);\n }\n\n #endFadeWindow(): void {\n clearTimeout(this.#fadeTimer);\n this.#fadeTimer = undefined;\n this.#fadeEndsAt = 0;\n this.#fadeOutFields.clear();\n this.#fadeInFields.clear();\n this.gridElement?.removeAttribute(COLUMN_FADE_ATTR);\n\n // The fade-out fills forwards to hold the cell at zero until the render that\n // hides it lands. Past the window CSS owns visibility, so releasing the fill\n // is what stops an abandoned fade from stranding a visible column invisible.\n for (const cell of this.gridElement?.querySelectorAll<HTMLElement>('.cell[data-field]') ?? []) {\n cancelColumnFades(cell);\n }\n }\n\n /** Animate one cell, seeking to `elapsed` so a cell created mid-fade joins in progress. */\n #playColumnFade(cell: HTMLElement, out: boolean, duration: number, elapsed: number): void {\n cancelColumnFades(cell);\n const frames = out ? [{ opacity: 1 }, { opacity: 0 }] : [{ opacity: 0 }, { opacity: 1 }];\n // A fade-out has to hold at zero until the closing render hides the cell for real.\n const animation = cell.animate?.(frames, { duration, easing: 'ease-out', fill: out ? 'forwards' : 'none' });\n if (!animation) return;\n animation.id = COLUMN_FADE_ID;\n animation.currentTime = elapsed;\n }\n\n /**\n * Collapse the tracks of hidden columns to zero width. Cells are auto-placed, so\n * a hidden cell has to stay in layout — dropping it would pull every later cell\n * into the wrong track. Collapsing the track instead is what lets the row, and\n * therefore the horizontal scroll width, shrink to the visible columns.\n *\n * Core rewrites `--tbw-column-template` from the full column list on every\n * template update, so this override is re-applied after each render.\n */\n #applyColumnTemplate(hidden: ReadonlySet<string>): void {\n const base = this.#internalGrid?._gridTemplate ?? '';\n if (!base) return;\n\n const columns = this.visibleColumns;\n const tracks = splitTrackList(base);\n let collapsed = false;\n\n if (hidden.size > 0 && tracks.length === columns.length) {\n for (let i = 0; i < columns.length; i++) {\n const field = columns[i]?.field;\n if (field && hidden.has(field)) {\n tracks[i] = collapsedTrack(tracks[i]);\n collapsed = true;\n }\n }\n }\n\n if (!collapsed && !this.#templateOverridden) return;\n this.gridElement.style.setProperty('--tbw-column-template', collapsed ? tracks.join(' ') : base);\n this.#templateOverridden = collapsed;\n }\n\n override detach(): void {\n this.#resizeObserver?.disconnect();\n this.#resizeObserver = undefined;\n this.#endFadeWindow();\n clearTimeout(this.#debounceTimer);\n this.#debounceTimer = undefined;\n this.#lastCheckedWidth = undefined;\n this.#lastSwitchAt = Number.NEGATIVE_INFINITY;\n\n // Every attribute `#syncLayoutAttributes()` writes styles the grid on its own\n // — `[data-responsive-animate]` transitions rows even in table mode — so a\n // detached plugin that leaves one behind keeps styling a grid it no longer owns.\n if (this.gridElement) {\n this.gridElement.removeAttribute('data-responsive');\n this.gridElement.removeAttribute('data-responsive-animate');\n this.gridElement.removeAttribute('data-responsive-hide-header');\n this.gridElement.removeAttribute('data-responsive-transition');\n for (const cell of this.gridElement.querySelectorAll(\n '.cell[data-responsive-hidden], .cell[data-responsive-value-only]',\n )) {\n cell.removeAttribute('data-responsive-hidden');\n cell.removeAttribute('data-responsive-value-only');\n cell.removeAttribute('aria-hidden');\n }\n this.#clearMorphNames();\n }\n this.#appliedHiddenFields = EMPTY_FIELD_SET;\n this.#appliedValueOnlyFields = EMPTY_FIELD_SET;\n\n super.detach();\n }\n\n /**\n * Handle plugin queries.\n * @internal\n */\n override handleQuery(query: PluginQuery): unknown {\n if (query.type === 'isCardMode') {\n return this.#isResponsive;\n }\n return undefined;\n }\n\n /**\n * Apply hidden and value-only columns.\n * In legacy mode (single breakpoint), only applies when in responsive mode.\n * In multi-breakpoint mode, applies whenever there's an active breakpoint.\n */\n override afterRender(): void {\n // Measure card height for virtualization calculations\n this.#measureCardHeightFromDOM();\n\n // In single breakpoint mode, only apply when responsive\n // In multi-breakpoint mode, apply when there's an active breakpoint\n const shouldApply = this.#sortedBreakpoints.length > 0 ? this.#activeBreakpoint !== null : this.#isResponsive;\n\n const hidden = shouldApply ? this.#hiddenColumnSet : EMPTY_FIELD_SET;\n const valueOnly = shouldApply ? this.#valueOnlyColumnSet : EMPTY_FIELD_SET;\n const fading = this.#fadeOutFields.size > 0 || this.#fadeInFields.size > 0;\n\n // Cells live in a recycled row pool, so an attribute written in card mode\n // outlives the switch back to table — one clearing pass is still owed.\n const stale = this.#appliedHiddenFields.size > 0 || this.#appliedValueOnlyFields.size > 0;\n\n // A layout switch changes which columns are hidden without going through the\n // config, and the switch animation already covers it — so record the new\n // truth here rather than letting the next config change diff against a stale set.\n this.#appliedHiddenFields = hidden;\n this.#appliedValueOnlyFields = valueOnly;\n\n // Card mode stacks cells vertically, so the track list is meaningless there.\n this.#applyColumnTemplate(this.#isResponsive ? EMPTY_FIELD_SET : hidden);\n\n if (hidden.size === 0 && valueOnly.size === 0 && !fading && !stale) {\n return;\n }\n\n const duration = this.#motionDurationMs;\n const elapsed = fading ? Math.min(duration, Math.max(0, duration - (this.#fadeEndsAt - performance.now()))) : 0;\n\n // Mark cells for hidden columns and value-only columns\n const cells = this.gridElement.querySelectorAll<HTMLElement>('.cell[data-field]');\n for (const cell of cells) {\n const field = cell.getAttribute('data-field');\n if (!field) continue;\n\n const fadingOut = this.#fadeOutFields.has(field);\n if (fadingOut || this.#fadeInFields.has(field)) this.#playColumnFade(cell, fadingOut, duration, elapsed);\n\n // The cell keeps its box and collapses with its track, so it stays out of\n // the accessibility tree the way `display: none` used to keep it.\n if (hidden.has(field)) {\n cell.setAttribute('data-responsive-hidden', '');\n cell.setAttribute('aria-hidden', 'true');\n cell.removeAttribute('data-responsive-value-only');\n }\n // Apply value-only attribute (shows value without header label)\n else if (valueOnly.has(field)) {\n cell.setAttribute('data-responsive-value-only', '');\n cell.removeAttribute('data-responsive-hidden');\n cell.removeAttribute('aria-hidden');\n }\n // Clear any previous responsive attributes\n else {\n cell.removeAttribute('data-responsive-hidden');\n cell.removeAttribute('data-responsive-value-only');\n cell.removeAttribute('aria-hidden');\n }\n }\n }\n\n /**\n * Check if width has crossed any breakpoint threshold.\n * Handles both single breakpoint (legacy) and multi-breakpoint modes.\n */\n #checkBreakpoint(width: number): void {\n // Multi-breakpoint mode\n if (this.#sortedBreakpoints.length > 0) {\n this.#checkMultiBreakpoint(width);\n return;\n }\n\n // Legacy single breakpoint mode\n const breakpoint = this.config.breakpoint ?? 0;\n\n // Warn once if breakpoint not configured (0 means never responsive)\n if (breakpoint === 0 && !this.#warnedAboutMissingBreakpoint) {\n this.#warnedAboutMissingBreakpoint = true;\n this.warn(\n MISSING_BREAKPOINT,\n \"No breakpoint configured. Responsive mode is disabled. Set a breakpoint based on your grid's column count.\",\n );\n }\n\n const shouldBeResponsive = breakpoint > 0 && width < breakpoint;\n\n if (shouldBeResponsive !== this.#isResponsive) {\n this.#isResponsive = shouldBeResponsive;\n this.#lastSwitchAt = performance.now();\n this.#commitLayoutSwitch();\n this.emit('responsive-change', {\n isResponsive: shouldBeResponsive,\n width,\n breakpoint,\n } satisfies ResponsiveChangeDetail);\n }\n }\n\n /**\n * Check breakpoints in multi-breakpoint mode.\n * Evaluates breakpoints from largest to smallest, applying the first match.\n */\n #checkMultiBreakpoint(width: number): void {\n // Find the active breakpoint (first one where width <= maxWidth)\n // Since sorted largest to smallest, we find the largest matching breakpoint\n let newActiveBreakpoint: BreakpointConfig | null = null;\n\n for (const bp of this.#sortedBreakpoints) {\n if (width <= bp.maxWidth) {\n newActiveBreakpoint = bp;\n // Continue to find the most specific (smallest) matching breakpoint\n }\n }\n\n // Check if breakpoint changed\n const breakpointChanged = newActiveBreakpoint !== this.#activeBreakpoint;\n\n if (breakpointChanged) {\n this.#activeBreakpoint = newActiveBreakpoint;\n this.#lastSwitchAt = performance.now();\n\n // Update hidden column sets from active breakpoint\n if (newActiveBreakpoint?.hiddenColumns) {\n this.#buildHiddenColumnSets(newActiveBreakpoint.hiddenColumns);\n } else {\n // Fall back to top-level hiddenColumns config\n this.#buildHiddenColumnSets(this.config.hiddenColumns);\n }\n\n // Determine if we should be in card layout\n const shouldBeResponsive = newActiveBreakpoint?.cardLayout === true;\n const layoutChanged = shouldBeResponsive !== this.#isResponsive;\n\n if (layoutChanged) {\n this.#isResponsive = shouldBeResponsive;\n }\n\n // A hidden-column change still needs a re-render even when the card\n // layout itself did not flip, so always commit through the same path.\n this.#commitLayoutSwitch(layoutChanged);\n\n // Emit event for any breakpoint change\n this.emit('responsive-change', {\n isResponsive: this.#isResponsive,\n width,\n breakpoint: newActiveBreakpoint?.maxWidth ?? 0,\n } satisfies ResponsiveChangeDetail);\n }\n }\n\n /**\n * Commit a layout switch, driving it through a view transition when the\n * browser supports element-scoped transitions.\n *\n * The switch is a wholesale layout change (table rows become cards), so the\n * compositor cross-fade is both cheaper and more legible than animating the\n * DOM. Where `Element.startViewTransition()` is unavailable the grid falls\n * back to the CSS keyframe fade driven by `data-responsive-animate`.\n *\n * A hidden-column change is deliberately excluded from the transition. Only the\n * affected column moves in that case — its grid track collapses to zero width\n * while every other track keeps its size — so the leaving cell can fade and\n * shrink in place. Snapshotting the whole grid for it makes all columns shimmer,\n * and because the change is driven by a resize the frozen snapshot is scaled\n * against a container that is still being dragged.\n * @param layoutChanged - Whether the table/card layout itself flipped. `false`\n * when only the hidden-column set changed.\n */\n #commitLayoutSwitch(layoutChanged = true): void {\n this.#syncMotionDuration();\n\n // The view transition already cross-fades everything, so per-column fades\n // would only fight it.\n if (layoutChanged) this.#endFadeWindow();\n\n const startViewTransition = layoutChanged ? this.#viewTransitionStarter() : null;\n if (!startViewTransition) {\n if (layoutChanged) this.#applyResponsiveState();\n this.requestRender();\n return;\n }\n\n const host = this.gridElement;\n const morph = this.#resolveMorphGranularity();\n const token = ++this.#transitionToken;\n\n // Names must exist on the outgoing rows before the snapshot is taken.\n if (morph !== 'none') this.#setMorphNames(morph);\n host.setAttribute('data-responsive-transition', '');\n\n const transition = startViewTransition(async () => {\n this.#applyResponsiveState();\n // The render scheduler batches into rAF, which still runs inside the\n // update callback — so awaiting here captures the finished new layout.\n await this.#internalGrid.forceLayout?.();\n if (morph !== 'none') this.#setMorphNames(morph);\n });\n\n const cleanup = (): void => {\n // A superseded transition still settles, and tearing down the names and\n // attribute its successor is mid-flight on would abort that successor.\n if (token !== this.#transitionToken) return;\n host.removeAttribute('data-responsive-transition');\n if (morph !== 'none') this.#clearMorphNames();\n };\n transition.finished.then(cleanup, cleanup);\n // A drag across the breakpoint supersedes the transition in flight; the UA\n // skips it and rejects `ready`, which is expected rather than an error.\n transition.ready?.catch(() => undefined);\n }\n\n /** Animation style in effect, resolving the deprecated `animate` flag. */\n get #animation(): ResponsiveAnimation {\n const { animation, animate } = this.config;\n if (animation !== undefined) return animation;\n return animate === false ? false : 'fade';\n }\n\n /** Duration every responsive animation runs at; `0` when motion is suppressed. */\n get #motionDurationMs(): number {\n const enabled = this.#animation !== false && this.isAnimationEnabled;\n return enabled ? (this.config.animationDuration ?? 200) : 0;\n }\n\n /**\n * Publish the single duration every responsive animation reads. Collapsing it\n * to `0ms` is how `animation: false` and `prefers-reduced-motion` silence the\n * CSS-driven paths (the column fade, the card-enter keyframe, and the view\n * transition groups), none of which can see the plugin config.\n */\n #syncMotionDuration(): void {\n this.gridElement.style.setProperty('--tbw-responsive-duration', `${this.#motionDurationMs}ms`);\n }\n\n /**\n * Resolve the element-scoped view transition entry point, or `null` when\n * animation is disabled or the browser does not support it.\n */\n #viewTransitionStarter(): ViewTransitionStarter | null {\n if (this.#animation === false || !this.isAnimationEnabled) return null;\n\n const host: ViewTransitionCapableElement = this.gridElement;\n const start = host.startViewTransition;\n return typeof start === 'function' ? start.bind(host) : null;\n }\n\n /** Whether the browser can drive the layout switch on the compositor. */\n get #supportsViewTransition(): boolean {\n const host: ViewTransitionCapableElement | undefined = this.gridElement;\n return typeof host?.startViewTransition === 'function';\n }\n\n /**\n * Give every rendered row a stable `view-transition-name` so the compositor\n * animates each row from its table position to its card position.\n */\n #setMorphNames(granularity: MorphGranularity): void {\n const rows = this.gridElement.querySelectorAll<HTMLElement>('.data-grid-row[aria-rowindex]');\n for (const row of rows) {\n const rowIndex = row.getAttribute('aria-rowindex');\n if (granularity === 'rows') {\n row.style.setProperty('view-transition-name', `${this.#morphPrefix}${rowIndex}`);\n continue;\n }\n // Only the leaves are named — naming the row too would lift its cells out\n // of the row's own snapshot and animate both against each other.\n for (const cell of row.querySelectorAll<HTMLElement>(':scope > .cell[aria-colindex]')) {\n const name = `${this.#cellMorphPrefix}${rowIndex}-${cell.getAttribute('aria-colindex')}`;\n cell.style.setProperty('view-transition-name', name);\n }\n }\n }\n\n /**\n * Pick the morph granularity for this switch.\n *\n * Cell identity is NOT stable across a layout switch (the row renderer\n * rebuilds cells whenever the visible column set changes), so names are\n * authored rather than left to `view-transition-name: match-element`.\n */\n #resolveMorphGranularity(): MorphGranularity {\n const animation = this.#animation;\n if (animation === 'morph-cells') {\n const cells = this.gridElement.querySelectorAll('.data-grid-row > .cell[aria-colindex]').length;\n if (cells > 0 && cells <= MAX_MORPH_CELLS) return 'cells';\n return 'rows';\n }\n return animation === 'morph-rows' ? 'rows' : 'none';\n }\n\n /** Remove morph names so recycled rows and cells do not carry stale identities. */\n #clearMorphNames(): void {\n const named = this.gridElement.querySelectorAll<HTMLElement>('.data-grid-row, .data-grid-row > .cell');\n for (const el of named) {\n el.style.removeProperty('view-transition-name');\n }\n }\n\n /** Original row height before entering responsive mode, for restoration on exit */\n #originalRowHeight?: number;\n\n /** The host attributes every responsive CSS rule keys off. */\n #syncLayoutAttributes(): void {\n this.gridElement.toggleAttribute('data-responsive', this.#isResponsive);\n\n // The keyframe fade is the no-view-transition path only. Leaving it on\n // would replay the card-enter animation the moment the transition ends.\n this.gridElement.toggleAttribute(\n 'data-responsive-animate',\n this.#animation !== false && !this.#supportsViewTransition,\n );\n\n // Attribute only meaningful while in card mode; clear it otherwise so\n // the CSS rule cannot accidentally apply to non-responsive layouts.\n this.gridElement.toggleAttribute(\n 'data-responsive-hide-header',\n this.#isResponsive && this.config.hideHeader === true,\n );\n }\n\n /**\n * Apply the responsive state to the grid element.\n * Handles scroll reset when entering responsive mode and row height restoration on exit.\n */\n #applyResponsiveState(): void {\n this.#syncLayoutAttributes();\n\n // Cast to internal type for virtualization access\n const internalGrid = this.#internalGrid;\n\n if (this.#isResponsive) {\n // Store original row height before responsive mode changes it\n if (internalGrid._virtualization) {\n this.#originalRowHeight = internalGrid._virtualization.rowHeight;\n }\n\n // Reset horizontal scroll position when entering responsive mode\n // The CSS hides overflow but doesn't reset the scroll position\n const scrollArea = this.gridElement.querySelector('.tbw-scroll-area') as HTMLElement | null;\n if (scrollArea) {\n scrollArea.scrollLeft = 0;\n }\n } else {\n // Exiting responsive mode - clean up inline styles set by renderRow\n // The rows are reused from the pool, so we need to remove the card-specific styles\n const rows = this.gridElement.querySelectorAll('.data-grid-row');\n for (const row of rows) {\n (row as HTMLElement).style.height = '';\n row.classList.remove('responsive-card');\n }\n\n // Restore original row height\n if (this.#originalRowHeight && this.#originalRowHeight > 0 && internalGrid._virtualization) {\n internalGrid._virtualization.rowHeight = this.#originalRowHeight;\n this.#originalRowHeight = undefined;\n }\n\n // Clear cached measurements so they're remeasured fresh when re-entering responsive mode\n // Without this, stale measurements cause incorrect height calculations after scrolling\n this.#measuredCardHeight = undefined;\n this.#measuredGroupRowHeight = undefined;\n this.#lastCardRowCount = undefined;\n }\n }\n\n /**\n * Custom row rendering when cardRenderer is provided and in responsive mode.\n *\n * When a cardRenderer is configured, this hook takes over row rendering to display\n * the custom card layout instead of the default cell structure.\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 rendered (prevents default), `void` for default rendering\n */\n override renderRow(row: unknown, rowEl: HTMLElement, rowIndex: number): boolean | void {\n // Only override when in responsive mode AND cardRenderer is provided\n if (!this.#isResponsive || !this.config.cardRenderer) {\n return; // Let default rendering proceed\n }\n\n // Skip group rows from GroupingRowsPlugin - they have special structure\n // and should use their own renderer\n if ((row as { __isGroupRow?: boolean }).__isGroupRow) {\n return; // Let GroupingRowsPlugin handle group row rendering\n }\n\n // Clear existing content\n rowEl.replaceChildren();\n\n // Call user's cardRenderer to get custom content\n const cardContent = this.config.cardRenderer(row as T, rowIndex);\n\n // Reset className - clears any stale classes from previous use (e.g., 'group-row' from recycled element)\n // This follows the same pattern as GroupingRowsPlugin which sets className explicitly\n rowEl.className = 'data-grid-row responsive-card';\n\n // Handle card row height — when a numeric height is configured, use the effective\n // height from #getCardHeight() which incorporates DOM measurement after first render.\n // This keeps inline height in sync with the position cache used for virtualization.\n const configuredHeight = this.config.cardRowHeight;\n if (configuredHeight === 'auto' || configuredHeight === undefined) {\n rowEl.style.height = 'auto';\n } else {\n rowEl.style.height = `${this.#getCardHeight()}px`;\n }\n\n // Append the custom card content\n // The row keeps `role=\"row\"`, which may only own cell-ish children (WCAG\n // 1.3.1 / axe `aria-required-children`). A card collapses the whole record\n // into one box, so it is exposed as a single spanning gridcell — unless the\n // renderer already assigned its own role.\n if (!cardContent.hasAttribute('role')) {\n cardContent.setAttribute('role', 'gridcell');\n cardContent.setAttribute('aria-colindex', '1');\n }\n rowEl.appendChild(cardContent);\n\n return true; // We handled rendering\n }\n\n /**\n * Handle keyboard navigation in responsive mode.\n *\n * In responsive mode, the visual layout is inverted:\n * - Cells are stacked vertically within each \"card\" (row)\n * - DOWN/UP visually moves within the card (between fields)\n * - Page Down/Page Up or Ctrl+Down/Up moves between cards\n *\n * For custom cardRenderers, keyboard navigation is disabled entirely\n * since the implementor controls the card content and should handle\n * navigation via their own event handlers.\n *\n * @returns `true` if the event was handled and default behavior should be prevented\n */\n override onKeyDown(e: KeyboardEvent): boolean {\n if (!this.#isResponsive) {\n return false;\n }\n\n // If custom cardRenderer is provided, disable grid's keyboard navigation\n // The implementor is responsible for their own navigation\n if (this.config.cardRenderer) {\n const navKeys = ['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'];\n if (navKeys.includes(e.key)) {\n // Let the event bubble - implementor can handle it\n return false;\n }\n }\n\n // Swap arrow key behavior for CSS-only responsive mode\n // In card layout, cells are stacked vertically:\n // Card 1: Card 2:\n // ID: 1 ID: 2\n // Name: Alice Name: Bob <- ArrowRight goes here\n // Dept: Eng Dept: Mkt\n // ↓ ArrowDown goes here\n //\n // ArrowDown/Up = move within card (change column/field)\n // ArrowRight/Left = move between cards (change row)\n const maxRow = this.rows.length - 1;\n const maxCol = this.visibleColumns.length - 1;\n\n switch (e.key) {\n case 'ArrowDown':\n // Move down WITHIN card (to next field/column)\n if (this.grid._focusCol < maxCol) {\n this.grid._focusCol += 1;\n e.preventDefault();\n ensureCellVisible(this.#internalGrid);\n return true;\n }\n // At bottom of card - optionally move to next card's first field\n if (this.grid._focusRow < maxRow) {\n this.grid._focusRow += 1;\n this.grid._focusCol = 0;\n e.preventDefault();\n ensureCellVisible(this.#internalGrid);\n return true;\n }\n break;\n\n case 'ArrowUp':\n // Move up WITHIN card (to previous field/column)\n if (this.grid._focusCol > 0) {\n this.grid._focusCol -= 1;\n e.preventDefault();\n ensureCellVisible(this.#internalGrid);\n return true;\n }\n // At top of card - optionally move to previous card's last field\n if (this.grid._focusRow > 0) {\n this.grid._focusRow -= 1;\n this.grid._focusCol = maxCol;\n e.preventDefault();\n ensureCellVisible(this.#internalGrid);\n return true;\n }\n break;\n\n case 'ArrowRight':\n // Move to NEXT card (same field)\n if (this.grid._focusRow < maxRow) {\n this.grid._focusRow += 1;\n e.preventDefault();\n ensureCellVisible(this.#internalGrid);\n return true;\n }\n break;\n\n case 'ArrowLeft':\n // Move to PREVIOUS card (same field)\n if (this.grid._focusRow > 0) {\n this.grid._focusRow -= 1;\n e.preventDefault();\n ensureCellVisible(this.#internalGrid);\n return true;\n }\n break;\n }\n\n return false;\n }\n\n // ============================================\n // Variable Height Support for Mixed Row Types\n // ============================================\n\n /** Measured card height from DOM for virtualization calculations */\n #measuredCardHeight?: number;\n\n /** Measured group row height from DOM for virtualization calculations */\n #measuredGroupRowHeight?: number;\n\n /** Last known card row count for detecting changes (e.g., group expand/collapse) */\n #lastCardRowCount?: number;\n\n /**\n * Get the effective card height for virtualization calculations.\n */\n #getCardHeight(): number {\n const configHeight = this.config.cardRowHeight;\n const measured = this.#measuredCardHeight;\n\n // A fixed height is the answer unless the content overflows it; only `auto`\n // hands the decision to the measurement outright.\n if (typeof configHeight === 'number' && configHeight > 0) {\n return measured && measured > configHeight ? measured : configHeight;\n }\n if (measured && measured > 0) {\n return measured;\n }\n return 80;\n }\n\n /**\n * Get the effective group row height for virtualization calculations.\n * Uses DOM-measured height, falling back to original row height.\n */\n #getGroupRowHeight(): number {\n if (this.#measuredGroupRowHeight && this.#measuredGroupRowHeight > 0) {\n return this.#measuredGroupRowHeight;\n }\n // Fall back to original row height (before responsive mode)\n return this.#originalRowHeight ?? 28;\n }\n\n /**\n * Check if there are any group rows in the current dataset.\n * Used to determine if we have mixed row heights.\n */\n #hasGroupRows(): boolean {\n for (const row of this.rows) {\n if ((row as { __isGroupRow?: boolean }).__isGroupRow) {\n return true;\n }\n }\n return false;\n }\n\n /**\n * Get the height of a specific row based on its type (group row vs card row).\n * Returns undefined if not in responsive mode.\n *\n * @param row - The row data\n * @param _index - The row index (unused, but part of the interface)\n * @returns The row height in pixels, or undefined if not in responsive mode\n */\n override getRowHeight(row: unknown, _index: number): number | undefined {\n // Only applies when in responsive mode with cardRenderer\n if (!this.#isResponsive || !this.config.cardRenderer) {\n return undefined;\n }\n\n // Check if this is a group row\n if ((row as { __isGroupRow?: boolean }).__isGroupRow) {\n return this.#getGroupRowHeight();\n }\n\n // Regular card row\n return this.#getCardHeight();\n }\n\n /**\n * Count the number of card rows (non-group rows) in the current dataset.\n */\n #countCardRows(): number {\n let count = 0;\n for (const row of this.rows) {\n if (!(row as { __isGroupRow?: boolean }).__isGroupRow) {\n count++;\n }\n }\n return count;\n }\n\n /** Pending refresh scheduled via microtask */\n #pendingRefresh = false;\n\n /**\n * Measure card height from DOM after render and detect row count changes.\n * Called in afterRender to ensure scroll calculations are accurate.\n *\n * This handles two scenarios:\n * 1. Card height changes (content overflow, dynamic sizing)\n * 2. Card row count changes (group expand/collapse)\n * 3. Group row height changes\n *\n * For uniform card layouts (no groups), we update the virtualization row height\n * directly to the card height. For mixed layouts (groups + cards), we use the\n * getExtraHeight mechanism to report height differences.\n *\n * The refresh is deferred via microtask to avoid nested render cycles.\n */\n #measureCardHeightFromDOM(): void {\n if (!this.#isResponsive) {\n return;\n }\n\n let needsRefresh = false;\n const internalGrid = this.#internalGrid;\n const hasGroups = this.#hasGroupRows();\n\n // Check if card row count changed (e.g., group expanded/collapsed)\n const currentCardRowCount = this.#countCardRows();\n if (currentCardRowCount !== this.#lastCardRowCount) {\n this.#lastCardRowCount = currentCardRowCount;\n needsRefresh = true;\n }\n\n // Measure actual group row height from DOM (for mixed layouts)\n if (hasGroups) {\n const groupRow = this.gridElement.querySelector('.data-grid-row.group-row') as HTMLElement | null;\n if (groupRow) {\n const height = groupRow.getBoundingClientRect().height;\n if (height > 0 && height !== this.#measuredGroupRowHeight) {\n this.#measuredGroupRowHeight = height;\n needsRefresh = true;\n }\n }\n }\n\n // Measure actual card height from DOM.\n // With cardRenderer, rows have the `.responsive-card` class.\n // Without cardRenderer (CSS-only card mode), rows are plain `.data-grid-row`\n // elements whose CSS height is `auto` — we need to measure their actual\n // rendered height so the faux scrollbar spacer is sized correctly.\n const cardSelector = this.config.cardRenderer ? '.data-grid-row.responsive-card' : '.data-grid-row:not(.group-row)';\n const cardRow = this.gridElement.querySelector(cardSelector) as HTMLElement | null;\n if (cardRow) {\n const height = cardRow.getBoundingClientRect().height;\n if (height > 0 && height !== this.#measuredCardHeight) {\n this.#measuredCardHeight = height;\n needsRefresh = true;\n\n // For uniform card layouts (no groups), update virtualization row height directly\n // This ensures proper row recycling and translateY calculations\n if (!hasGroups && internalGrid._virtualization) {\n internalGrid._virtualization.rowHeight = height;\n }\n }\n }\n\n // Defer virtualization refresh to avoid nested render cycles\n // This is called from afterRender, so we can't call refreshVirtualWindow synchronously\n // Use scheduler's VIRTUALIZATION phase to batch properly and avoid duplicate afterRender calls\n if (needsRefresh && !this.#pendingRefresh) {\n this.#pendingRefresh = true;\n queueMicrotask(() => {\n this.#pendingRefresh = false;\n // Only refresh if still attached and in responsive mode\n if (this.grid && this.#isResponsive) {\n // Request virtualization phase through grid's public API\n // This goes through the scheduler which batches and handles afterRender properly\n this.#internalGrid.refreshVirtualWindow?.(true, true);\n }\n });\n }\n }\n}\n"],"mappings":"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"}
|