@toolbox-web/grid 3.9.1 → 3.10.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.js +1 -1
- package/all.js.map +1 -1
- package/custom-elements.json +2 -2
- package/index.js +1 -1
- package/index.js.map +1 -1
- package/lib/core/grid.d.ts +8 -4
- package/lib/core/plugin/base-plugin.d.ts +7 -0
- package/lib/core/types.d.ts +8 -3
- package/lib/features/clipboard.js.map +1 -1
- package/lib/features/column-virtualization.js.map +1 -1
- package/lib/features/context-menu.js.map +1 -1
- package/lib/features/editing.js.map +1 -1
- package/lib/features/export.js.map +1 -1
- package/lib/features/filtering.js.map +1 -1
- package/lib/features/grouping-columns.js.map +1 -1
- package/lib/features/grouping-rows.js.map +1 -1
- package/lib/features/master-detail.js.map +1 -1
- package/lib/features/multi-sort.js.map +1 -1
- package/lib/features/pinned-columns.js.map +1 -1
- package/lib/features/pinned-rows.js.map +1 -1
- package/lib/features/pivot.js.map +1 -1
- package/lib/features/print.js.map +1 -1
- package/lib/features/registry.js +1 -1
- package/lib/features/registry.js.map +1 -1
- package/lib/features/reorder-columns.js.map +1 -1
- package/lib/features/responsive.js.map +1 -1
- package/lib/features/row-drag-drop.js.map +1 -1
- package/lib/features/selection.js.map +1 -1
- package/lib/features/server-side.js.map +1 -1
- package/lib/features/shell.js.map +1 -1
- package/lib/features/sticky-rows.js.map +1 -1
- package/lib/features/tooltip.js.map +1 -1
- package/lib/features/tree.js.map +1 -1
- package/lib/features/undo-redo.js.map +1 -1
- package/lib/features/visibility.js.map +1 -1
- package/lib/plugins/clipboard/index.js.map +1 -1
- package/lib/plugins/column-virtualization/index.js.map +1 -1
- package/lib/plugins/context-menu/context-menu-plugin.d.ts +9 -4
- package/lib/plugins/context-menu/index.js +1 -1
- package/lib/plugins/context-menu/index.js.map +1 -1
- package/lib/plugins/context-menu/types.d.ts +8 -4
- package/lib/plugins/editing/editors.d.ts +15 -0
- package/lib/plugins/editing/index.js +1 -1
- package/lib/plugins/editing/index.js.map +1 -1
- package/lib/plugins/editing/internal/dirty-tracking-manager.d.ts +2 -1
- package/lib/plugins/editing/types.d.ts +5 -1
- package/lib/plugins/export/index.js +1 -1
- package/lib/plugins/export/index.js.map +1 -1
- package/lib/plugins/filtering/filter-model.d.ts +22 -2
- package/lib/plugins/filtering/filter-panel-default.d.ts +15 -1
- package/lib/plugins/filtering/filtering-plugin.d.ts +5 -2
- package/lib/plugins/filtering/index.d.ts +1 -1
- package/lib/plugins/filtering/index.js +1 -1
- package/lib/plugins/filtering/index.js.map +1 -1
- package/lib/plugins/filtering/types.d.ts +58 -3
- package/lib/plugins/grouping-columns/index.js.map +1 -1
- package/lib/plugins/grouping-rows/grouping-rows-plugin.d.ts +7 -14
- package/lib/plugins/grouping-rows/grouping-rows.d.ts +48 -0
- 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 +21 -0
- package/lib/plugins/hierarchy/index.js.map +1 -1
- package/lib/plugins/master-detail/index.d.ts +1 -1
- package/lib/plugins/master-detail/index.js +1 -1
- package/lib/plugins/master-detail/index.js.map +1 -1
- package/lib/plugins/master-detail/master-detail-plugin.d.ts +28 -6
- package/lib/plugins/master-detail/master-detail.d.ts +0 -8
- package/lib/plugins/master-detail/types.d.ts +25 -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.map +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/pivot/pivot-plugin.d.ts +2 -0
- package/lib/plugins/pivot/types.d.ts +2 -0
- package/lib/plugins/print/index.js.map +1 -1
- package/lib/plugins/reorder-columns/index.js.map +1 -1
- package/lib/plugins/responsive/index.js.map +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/selection/selection-plugin.d.ts +8 -1
- package/lib/plugins/server-side/datasource-types.d.ts +28 -0
- package/lib/plugins/server-side/datasource.d.ts +7 -0
- package/lib/plugins/server-side/index.js +1 -1
- package/lib/plugins/server-side/index.js.map +1 -1
- package/lib/plugins/server-side/server-side-plugin.d.ts +61 -8
- package/lib/plugins/server-side/types.d.ts +9 -1
- package/lib/plugins/shared/form-field-target.d.ts +10 -0
- package/lib/plugins/shared/format-value.d.ts +12 -0
- package/lib/plugins/shell/index.js.map +1 -1
- package/lib/plugins/sticky-rows/index.js.map +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-plugin.d.ts +5 -11
- package/lib/plugins/tree/types.d.ts +22 -6
- package/lib/plugins/undo-redo/history.d.ts +2 -1
- package/lib/plugins/undo-redo/index.js +1 -1
- package/lib/plugins/undo-redo/index.js.map +1 -1
- package/lib/plugins/undo-redo/types.d.ts +13 -1
- package/lib/plugins/undo-redo/undo-redo-plugin.d.ts +15 -2
- 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/clipboard.umd.js.map +1 -1
- package/umd/plugins/column-virtualization.umd.js.map +1 -1
- package/umd/plugins/context-menu.umd.js +1 -1
- package/umd/plugins/context-menu.umd.js.map +1 -1
- package/umd/plugins/editing.umd.js +1 -1
- package/umd/plugins/editing.umd.js.map +1 -1
- package/umd/plugins/export.umd.js +1 -1
- package/umd/plugins/export.umd.js.map +1 -1
- package/umd/plugins/filtering.umd.js +1 -1
- package/umd/plugins/filtering.umd.js.map +1 -1
- package/umd/plugins/grouping-columns.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.map +1 -1
- package/umd/plugins/master-detail.umd.js +1 -1
- package/umd/plugins/master-detail.umd.js.map +1 -1
- package/umd/plugins/multi-sort.umd.js +1 -1
- package/umd/plugins/multi-sort.umd.js.map +1 -1
- package/umd/plugins/pinned-columns.umd.js.map +1 -1
- package/umd/plugins/pinned-rows.umd.js.map +1 -1
- package/umd/plugins/pivot.umd.js +1 -1
- package/umd/plugins/pivot.umd.js.map +1 -1
- package/umd/plugins/print.umd.js.map +1 -1
- package/umd/plugins/reorder-columns.umd.js.map +1 -1
- package/umd/plugins/responsive.umd.js.map +1 -1
- package/umd/plugins/row-drag-drop.umd.js.map +1 -1
- package/umd/plugins/selection.umd.js +1 -1
- package/umd/plugins/selection.umd.js.map +1 -1
- package/umd/plugins/server-side.umd.js +1 -1
- package/umd/plugins/server-side.umd.js.map +1 -1
- package/umd/plugins/shell.umd.js.map +1 -1
- package/umd/plugins/sticky-rows.umd.js.map +1 -1
- package/umd/plugins/tooltip.umd.js.map +1 -1
- package/umd/plugins/tree.umd.js +1 -1
- package/umd/plugins/tree.umd.js.map +1 -1
- package/umd/plugins/undo-redo.umd.js +1 -1
- package/umd/plugins/undo-redo.umd.js.map +1 -1
- package/umd/plugins/visibility.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/keyboard.ts","../../../../../../libs/grid/src/lib/core/internal/sanitize.ts","../../../../../../libs/grid/src/lib/plugins/shared/loading-placeholder.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 * 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 * Loading placeholder rows shared by plugins that synthesize them (ServerSide\n * blocks in flight, GroupingRows groups being fetched). Inlined into each\n * plugin bundle that imports it — core never calls it.\n */\nimport { setRowLoadingState } from '../../core/internal/loading';\nimport { clearRowForRebuild } from '../../core/internal/row-teardown';\nimport type { GridHost, RowElementInternal } from '../../core/types';\n\n/**\n * Marker class on placeholder rows. Lets layout plugins (Responsive card mode)\n * tell a placeholder apart from a data row — `tbw-row-loading` is not enough,\n * because `grid.setRowLoading()` puts that on real rows too.\n */\nexport const PLACEHOLDER_ROW_CLASS = 'placeholder-row';\n\n/**\n * Render a placeholder as one full-width cell in the core row-loading state, so\n * no column renderer runs for a row that holds no data.\n *\n * Call from a plugin `renderRow` hook and return `true`. The previous cells are\n * released through the framework adapter first (a pooled element may have held\n * React portals / Vue teleports / Angular views for a loaded row). The row is\n * flagged `__isCustomRow`, which forces a full rebuild once real data replaces\n * the placeholder; that rebuild also clears the loading state.\n *\n * Height: the row occupies exactly what virtualization reserves for it — the\n * height a plugin reports through `getRowHeight()` (e.g. a Responsive card), else\n * the default row height. Any inline height a recycled element carried (a\n * Responsive card height, a per-row `--tbw-row-height`) is dropped.\n */\nexport function renderLoadingPlaceholderRow(grid: GridHost, rowEl: HTMLElement, row: unknown, rowIndex: number): void {\n const el = rowEl as RowElementInternal;\n syncPlaceholderHeight(grid, el, row, rowIndex);\n // Same placeholder, still intact: nothing else to do (renders run every scroll frame).\n if (el.__rowDataRef === row && el.__isCustomRow && el.classList.contains('tbw-row-loading')) return;\n\n clearRowForRebuild(grid, el);\n el.className = `data-grid-row ${PLACEHOLDER_ROW_CLASS}`;\n el.__isCustomRow = true;\n const cell = document.createElement('div');\n cell.className = 'cell';\n cell.style.gridColumn = '1 / -1';\n cell.setAttribute('role', 'gridcell');\n cell.textContent = '\\u00A0'; // keeps the row at its normal height\n el.appendChild(cell);\n setRowLoadingState(el, true, grid.effectiveConfig?.loadingRenderer);\n}\n\n/** Match the row's DOM height to the slot virtualization reserves for it. Writes only on change. */\nfunction syncPlaceholderHeight(grid: GridHost, el: HTMLElement, row: unknown, rowIndex: number): void {\n const { style } = el;\n if (style.getPropertyValue('--tbw-row-height')) style.removeProperty('--tbw-row-height');\n const pluginHeight = grid._getPluginRowHeight?.(row, rowIndex);\n const height = pluginHeight !== undefined && pluginHeight > 0 ? `${pluginHeight}px` : '';\n if (style.height !== height) style.height = height;\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`, and a loading placeholder takes\n the card height the plugin reports for it, so neither may be forced open. */\n .data-grid-row:not(.group-row, .responsive-card, .placeholder-row) {\n min-height: auto !important;\n height: auto !important;\n }\n\n /* Stack cells vertically as label: value pairs. A placeholder's single spinner\n cell has no field, so it gets no `Header:` label. */\n .data-grid-row:not(.group-row, .placeholder-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 { PLACEHOLDER_ROW_CLASS } from '../shared/loading-placeholder';\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 if (!this.#isResponsive) return undefined;\n\n // A loading placeholder (ServerSide block / grouped rows in flight) is drawn by\n // the plugin that owns it at the height reported here, so it reserves the slot\n // the real card will fill — no layout shift when the data arrives. A\n // `cardRenderer` card is sized by `cardRowHeight`; a CSS-only card grows to fit\n // its cells, so there the placeholder takes the MEASURED card height (until the\n // first measurement it falls back to the grid's estimate).\n if ((row as { __loading?: boolean }).__loading === true) {\n return this.config.cardRenderer ? this.#getCardHeight() : this.#measuredCardHeight;\n }\n\n // Otherwise only applies with a cardRenderer\n if (!this.config.cardRenderer) return undefined;\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. Loading\n // placeholders are sized FROM this measurement, so they must not feed it.\n const cardSelector = this.config.cardRenderer\n ? '.data-grid-row.responsive-card'\n : `.data-grid-row:not(.group-row, .${PLACEHOLDER_ROW_CLASS})`;\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":["styles","BaseGridPlugin","fromVirtualScrollTop","toVirtualScrollTop","GridClasses","ensureCellVisible","grid","options","_virtualization","enabled","rowHeight","container","viewportEl","scrollMapping","scrollEl","visibleHeight","clientHeight","offsets","_getVerticalScrollOffsets","_focusRow","top","bottom","skipScroll","Math","max","usableHeight","y","virtualScrollTop","scrollTop","target","scrollFocusedRowIntoView","isEditing","_activeEditRows","isRowEditing","_isGridEditMode","root","refreshVirtualWindow","_bodyEl","querySelectorAll","forEach","el","classList","remove","Array","from","setAttribute","focused","rowIndex","vStart","start","vEnd","end","_rows","length","rowEl","cell","children","_focusCol","contains","querySelector","findFocusedCell","add","forceHorizontalScroll","scrollArea","forceScrollLeft","scrollLeft","forceScrollRight","scrollWidth","clientWidth","_getHorizontalScrollOffsets","left","right","cellRect","getBoundingClientRect","scrollAreaRect","cellLeft","cellRight","width","visibleLeft","visibleRight","scrollCellIntoViewHorizontally","focusTarget","document","activeElement","focus","preventScroll","hasAttribute","active","HTMLElement","closest","applyFocusTarget","EXPR_RE","EMPTY_SENTINEL","SAFE_EXPR","_F","split","map","s","reverse","join","FORBIDDEN","RegExp","slice","DANGEROUS_TAGS","Set","DANGEROUS_ATTR_PATTERN","URL_ATTRS","DANGEROUS_URL_PROTOCOL","sanitizeToFragment","html","template","createElement","innerHTML","toRemove","elements","tagName","toLowerCase","has","push","namespaceURI","attributes","some","attr","test","name","attrsToRemove","attrName","value","removeAttribute","sanitizeNode","content","safeEval","expr","ctx","toks","tokens","i","ch","type","v","Number","tri","bi","includes","tokenize","pos","peek","advance","match","val","t","parseTernary","result","parseLogicalAnd","parseLogicalOr","consequent","alternate","parseEquality","parseComparison","parseAdditive","parseMultiplicative","parseUnary","parsePrimary","startsWith","key","row","typeDefault","resolveId","evalTemplateString","raw","indexOf","parts","finalStr","replace","_m","res","trim","REFLECTIVE_RE","String","dotChain","out","str","evalSingle","text","allEmpty","every","p","morphInstanceCounter","EMPTY_FIELD_SET","COLUMN_FADE_ID","COLUMN_FADE_ATTR","cancelColumnFades","animation","getAnimations","id","finished","catch","cancel","collapsedTrack","track","endsWith","ResponsivePlugin","version","styles","static","queries","description","resizeObserver","isResponsive","debounceTimer","warnedAboutMissingBreakpoint","currentWidth","lastCheckedWidth","lastSwitchAt","NEGATIVE_INFINITY","morphInstance","morphPrefix","this","cellMorphPrefix","transitionToken","hiddenColumnSet","appliedHiddenFields","valueOnlyColumnSet","appliedValueOnlyFields","fadeOutFields","fadeInFields","fadeEndsAt","fadeTimer","templateOverridden","activeBreakpoint","sortedBreakpoints","internalGrid","setResponsive","commitLayoutSwitch","emit","breakpoint","config","setBreakpoint","checkBreakpoint","setCardRenderer","renderer","cardRenderer","requestRender","getWidth","getActiveBreakpoint","attach","super","parseLightDomCard","buildHiddenColumnSets","hiddenColumns","breakpoints","sort","a","b","maxWidth","syncMotionDuration","syncLayoutAttributes","measuredCardHeight","measuredGroupRowHeight","ResizeObserver","entries","onResize","contentRect","observe","gridElement","debounceMs","sinceLastSwitch","performance","now","clearTimeout","setTimeout","gridEl","cardEl","adapter","__frameworkAdapter","parseResponsiveCardElement","adapterRenderer","breakpointAttr","getAttribute","cardRowHeightAttr","hiddenColumnsAttr","hideHeaderAttr","debounceMsAttr","configUpdates","parseInt","isNaN","cardRowHeight","filter","hideHeader","templateHTML","evaluated","className","replaceChildren","textContent","Object","keys","clear","col","showValue","field","queueHiddenColumnFades","hiddenColumnsApply","applied","markColumnFade","openFadeWindow","delete","duration","motionDurationMs","size","endFadeWindow","playColumnFade","elapsed","frames","opacity","animate","easing","fill","currentTime","applyColumnTemplate","hidden","base","_gridTemplate","columns","visibleColumns","tracks","depth","char","splitTrackList","collapsed","style","setProperty","detach","disconnect","clearMorphNames","handleQuery","query","afterRender","measureCardHeightFromDOM","shouldApply","valueOnly","fading","stale","min","cells","fadingOut","checkMultiBreakpoint","warn","shouldBeResponsive","newActiveBreakpoint","bp","cardLayout","layoutChanged","startViewTransition","viewTransitionStarter","applyResponsiveState","host","morph","resolveMorphGranularity","token","setMorphNames","transition","async","forceLayout","cleanup","then","ready","isAnimationEnabled","animationDuration","bind","supportsViewTransition","granularity","rows","named","removeProperty","originalRowHeight","toggleAttribute","height","lastCardRowCount","renderRow","__isGroupRow","cardContent","configuredHeight","getCardHeight","appendChild","onKeyDown","e","maxRow","maxCol","preventDefault","configHeight","measured","getGroupRowHeight","hasGroupRows","getRowHeight","_index","__loading","countCardRows","count","pendingRefresh","needsRefresh","hasGroups","currentCardRowCount","groupRow","cardSelector","cardRow","queueMicrotask"],"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/keyboard.ts","../../../../../../libs/grid/src/lib/core/internal/sanitize.ts","../../../../../../libs/grid/src/lib/plugins/shared/loading-placeholder.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 * 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 * Loading placeholder rows shared by plugins that synthesize them (ServerSide\n * blocks in flight, GroupingRows groups being fetched). Inlined into each\n * plugin bundle that imports it — core never calls it.\n */\nimport { setRowLoadingState } from '../../core/internal/loading';\nimport { clearRowForRebuild } from '../../core/internal/row-teardown';\nimport type { GridHost, RowElementInternal } from '../../core/types';\n\n/**\n * Marker class on placeholder rows. Lets layout plugins (Responsive card mode)\n * tell a placeholder apart from a data row — `tbw-row-loading` is not enough,\n * because `grid.setRowLoading()` puts that on real rows too.\n */\nexport const PLACEHOLDER_ROW_CLASS = 'placeholder-row';\n\n/**\n * Render a placeholder as one full-width cell in the core row-loading state, so\n * no column renderer runs for a row that holds no data.\n *\n * Call from a plugin `renderRow` hook and return `true`. The previous cells are\n * released through the framework adapter first (a pooled element may have held\n * React portals / Vue teleports / Angular views for a loaded row). The row is\n * flagged `__isCustomRow`, which forces a full rebuild once real data replaces\n * the placeholder; that rebuild also clears the loading state.\n *\n * Height: the row occupies exactly what virtualization reserves for it — the\n * height a plugin reports through `getRowHeight()` (e.g. a Responsive card), else\n * the default row height. Any inline height a recycled element carried (a\n * Responsive card height, a per-row `--tbw-row-height`) is dropped.\n */\nexport function renderLoadingPlaceholderRow(grid: GridHost, rowEl: HTMLElement, row: unknown, rowIndex: number): void {\n const el = rowEl as RowElementInternal;\n syncPlaceholderHeight(grid, el, row, rowIndex);\n // Same placeholder, still intact: nothing else to do (renders run every scroll frame).\n if (el.__rowDataRef === row && el.__isCustomRow && el.classList.contains('tbw-row-loading')) return;\n\n clearRowForRebuild(grid, el);\n el.className = `data-grid-row ${PLACEHOLDER_ROW_CLASS}`;\n el.__isCustomRow = true;\n const cell = document.createElement('div');\n cell.className = 'cell';\n cell.style.gridColumn = '1 / -1';\n cell.setAttribute('role', 'gridcell');\n cell.textContent = '\\u00A0'; // keeps the row at its normal height\n el.appendChild(cell);\n setRowLoadingState(el, true, grid.effectiveConfig?.loadingRenderer);\n}\n\n/** Match the row's DOM height to the slot virtualization reserves for it. Writes only on change. */\nfunction syncPlaceholderHeight(grid: GridHost, el: HTMLElement, row: unknown, rowIndex: number): void {\n const { style } = el;\n if (style.getPropertyValue('--tbw-row-height')) style.removeProperty('--tbw-row-height');\n const pluginHeight = grid._getPluginRowHeight?.(row, rowIndex);\n const height = pluginHeight !== undefined && pluginHeight > 0 ? `${pluginHeight}px` : '';\n if (style.height !== height) style.height = height;\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`, and a loading placeholder takes\n the card height the plugin reports for it, so neither may be forced open. */\n .data-grid-row:not(.group-row, .responsive-card, .placeholder-row) {\n min-height: auto !important;\n height: auto !important;\n }\n\n /* Stack cells vertically as label: value pairs. A placeholder's single spinner\n cell has no field, so it gets no `Header:` label. */\n .data-grid-row:not(.group-row, .placeholder-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 { PLACEHOLDER_ROW_CLASS } from '../shared/loading-placeholder';\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 if (!this.#isResponsive) return undefined;\n\n // A loading placeholder (ServerSide block / grouped rows in flight) is drawn by\n // the plugin that owns it at the height reported here, so it reserves the slot\n // the real card will fill — no layout shift when the data arrives. A\n // `cardRenderer` card is sized by `cardRowHeight`; a CSS-only card grows to fit\n // its cells, so there the placeholder takes the MEASURED card height (until the\n // first measurement it falls back to the grid's estimate).\n if ((row as { __loading?: boolean }).__loading === true) {\n return this.config.cardRenderer ? this.#getCardHeight() : this.#measuredCardHeight;\n }\n\n // Otherwise only applies with a cardRenderer\n if (!this.config.cardRenderer) return undefined;\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. Loading\n // placeholders are sized FROM this measurement, so they must not feed it.\n const cardSelector = this.config.cardRenderer\n ? '.data-grid-row.responsive-card'\n : `.data-grid-row:not(.group-row, .${PLACEHOLDER_ROW_CLASS})`;\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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"}
|