@toolbox-web/grid 3.0.0-beta → 3.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/all.js +2 -2
  2. package/all.js.map +1 -1
  3. package/index.js +1 -1
  4. package/index.js.map +1 -1
  5. package/lib/core/grid.d.ts +2 -1
  6. package/lib/core/internal/column-shorthand.d.ts +61 -0
  7. package/lib/core/internal/columns.d.ts +10 -0
  8. package/lib/core/types.d.ts +3 -0
  9. package/lib/features/registry.js +1 -1
  10. package/lib/features/registry.js.map +1 -1
  11. package/lib/plugins/clipboard/index.js +1 -1
  12. package/lib/plugins/clipboard/index.js.map +1 -1
  13. package/lib/plugins/column-virtualization/index.js +1 -1
  14. package/lib/plugins/column-virtualization/index.js.map +1 -1
  15. package/lib/plugins/context-menu/index.js +1 -1
  16. package/lib/plugins/context-menu/index.js.map +1 -1
  17. package/lib/plugins/editing/index.js +1 -1
  18. package/lib/plugins/editing/index.js.map +1 -1
  19. package/lib/plugins/export/index.js +1 -1
  20. package/lib/plugins/export/index.js.map +1 -1
  21. package/lib/plugins/filtering/index.js +1 -1
  22. package/lib/plugins/filtering/index.js.map +1 -1
  23. package/lib/plugins/grouping-columns/index.js +1 -1
  24. package/lib/plugins/grouping-columns/index.js.map +1 -1
  25. package/lib/plugins/grouping-rows/index.js +1 -1
  26. package/lib/plugins/grouping-rows/index.js.map +1 -1
  27. package/lib/plugins/master-detail/index.js +1 -1
  28. package/lib/plugins/master-detail/index.js.map +1 -1
  29. package/lib/plugins/multi-sort/index.js +1 -1
  30. package/lib/plugins/multi-sort/index.js.map +1 -1
  31. package/lib/plugins/pinned-columns/index.js +1 -1
  32. package/lib/plugins/pinned-columns/index.js.map +1 -1
  33. package/lib/plugins/pinned-rows/index.js +1 -1
  34. package/lib/plugins/pinned-rows/index.js.map +1 -1
  35. package/lib/plugins/pivot/index.js +1 -1
  36. package/lib/plugins/pivot/index.js.map +1 -1
  37. package/lib/plugins/print/index.js +1 -1
  38. package/lib/plugins/print/index.js.map +1 -1
  39. package/lib/plugins/reorder-columns/index.js +1 -1
  40. package/lib/plugins/reorder-columns/index.js.map +1 -1
  41. package/lib/plugins/responsive/index.js +1 -1
  42. package/lib/plugins/responsive/index.js.map +1 -1
  43. package/lib/plugins/row-drag-drop/index.js +1 -1
  44. package/lib/plugins/row-drag-drop/index.js.map +1 -1
  45. package/lib/plugins/selection/index.js +1 -1
  46. package/lib/plugins/selection/index.js.map +1 -1
  47. package/lib/plugins/server-side/index.js +1 -1
  48. package/lib/plugins/server-side/index.js.map +1 -1
  49. package/lib/plugins/shell/index.js +1 -1
  50. package/lib/plugins/shell/index.js.map +1 -1
  51. package/lib/plugins/sticky-rows/index.js +1 -1
  52. package/lib/plugins/sticky-rows/index.js.map +1 -1
  53. package/lib/plugins/tooltip/index.js +1 -1
  54. package/lib/plugins/tooltip/index.js.map +1 -1
  55. package/lib/plugins/tree/index.js +1 -1
  56. package/lib/plugins/tree/index.js.map +1 -1
  57. package/lib/plugins/undo-redo/index.js +1 -1
  58. package/lib/plugins/undo-redo/index.js.map +1 -1
  59. package/lib/plugins/visibility/index.js +1 -1
  60. package/lib/plugins/visibility/index.js.map +1 -1
  61. package/package.json +1 -1
  62. package/public.d.ts +2 -0
  63. package/umd/grid.all.umd.js +1 -1
  64. package/umd/grid.all.umd.js.map +1 -1
  65. package/umd/grid.umd.js +1 -1
  66. package/umd/grid.umd.js.map +1 -1
  67. package/umd/plugins/editing.umd.js +1 -1
  68. package/umd/plugins/editing.umd.js.map +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":["../../../../../../libs/grid/src/lib/core/internal/diagnostics.ts","../../../../../../libs/grid/src/lib/core/internal/sanitize.ts","../../../../../../libs/grid/src/lib/core/types.ts","../../../../../../libs/grid/src/lib/core/plugin/base-plugin.ts","../../../../../../libs/grid/src/lib/core/constants.ts","../../../../../../libs/grid/src/lib/core/internal/dom-builder.ts","../../../../../../libs/grid/src/lib/plugins/shell/shell.ts","../../../../../../libs/grid/src/lib/plugins/shell/shell-controller.ts","../../../../../../libs/grid/src/lib/plugins/shell/ShellPlugin.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, deprecation)\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 */\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 or title. */\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","// Centralized template expression evaluation & sanitization utilities.\n// Responsible for safely interpolating {{ }} expressions while blocking\n// access to dangerous globals / reflective capabilities.\nimport type { CompiledViewFunction, EvalContext } from '../types';\n\n// #region Constants\nconst EXPR_RE = /{{\\s*([^}]+)\\s*}}/g;\nconst EMPTY_SENTINEL = '__DG_EMPTY__';\nconst SAFE_EXPR = /^[\\w$. '?+\\-*/%:()!<>=,&|]+$/;\n\n// Forbidden identifiers built at runtime to avoid static analysis scanners\n// from flagging string literals (e.g. socket.dev \"network access\" / \"uses eval\").\n// Stored as reversed, pipe-delimited tokens — reversed back when constructing the regex.\nconst _F =\n '__otorp__|__retteGenifed__|__retteSenifed__|rotcurtsnoc|wodniw|sihTlabolg|labolg|ssecorp|noitcnuF|tropmi|lave|tcelfeR|yxorP|rorrE|stnemugra|tnemucod|noitacol|eikooc|egarotSlacol|egarotSnoisses|BDdexedni|hctef|tseuqeRpttHLMX|tekcoSbeW|rekroW|rekroWderahS|rekroWecivreS|renepo|tnerap|pot|semarf|fles'\n .split('|')\n .map((s) => s.split('').reverse().join(''));\nconst FORBIDDEN = new RegExp(`__(proto|defineGetter|defineSetter)|${_F.slice(3).join('|')}|this\\\\b`);\n// #endregion\n\n// #region HTML Sanitization\n\n/**\n * Escape a plain text string for safe insertion into HTML.\n * Converts special HTML characters to their entity equivalents.\n *\n * @param text - Plain text string to escape\n * @returns HTML-safe string\n */\nexport function escapeHtml(text: string): string {\n if (!text || typeof text !== 'string') return '';\n return text\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;')\n .replace(/'/g, '&#39;');\n}\n\n/**\n * Tags that are considered dangerous and will be completely removed.\n * These can execute scripts, load external resources, or manipulate the page.\n */\nconst DANGEROUS_TAGS = new Set(\n 'script|iframe|object|embed|form|input|button|textarea|select|link|meta|base|style|template|slot|portal|frame|frameset|applet|noscript|noembed|plaintext|xmp|listing'.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 * This is a defense-in-depth measure for content rendered via innerHTML.\n *\n * **When to use:** ANY path that writes a user-supplied string to `innerHTML`\n * MUST pass through this function — `gridConfig.icons.*` (icon strings can be\n * SVG markup), cell renderer string returns, group-header renderer string\n * returns, light-DOM tool panel fallback content, plugin wrappers, and any new\n * extension point that turns a user string into markup. Using `textContent` is\n * wrong here (it renders HTML/SVG icon strings as literal text); using raw\n * `innerHTML` is wrong (it is an XSS sink). The canonical reference\n * implementation is `core/internal/rows.ts` (cell renderer path) — mirror it.\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 * 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 // 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 }\n | { type: 'str'; v: string }\n | { type: 'id'; v: string }\n | { 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 // bare 'row' reference\n if (name === 'row') return ctx.row;\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 return 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.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","import type { ToolPanelDefinition } from '../plugins/shell/types';\nimport type { RenderPhase } from './internal/render-scheduler';\nimport type { RowPosition, ScrollMapping } from './internal/virtualization';\nimport type { AfterCellRenderContext, AfterRowRenderContext, CellMouseEvent } from './plugin/types';\n\n/**\n * Position entry for a single row in the position cache.\n * Part of variable row height virtualization.\n *\n * Re-exported from position-cache.ts for public API consistency.\n *\n * @see VirtualState.positionCache\n * @category Plugin Development\n */\nexport type RowPositionEntry = RowPosition;\n\n// #region ScrollToRowOptions\n/**\n * Options for the {@link PublicGrid.scrollToRow} method.\n *\n * @group Focus & Navigation\n *\n * @example\n * ```typescript\n * grid.scrollToRow(42, { align: 'center', behavior: 'smooth' });\n * ```\n * @since 1.23.0\n */\nexport interface ScrollToRowOptions {\n /**\n * Where to position the row in the viewport.\n *\n * - `'start'` — top of the viewport\n * - `'center'` — vertically centered\n * - `'end'` — bottom of the viewport\n * - `'nearest'` — scroll only if the row is outside the viewport (default)\n *\n * @defaultValue `'nearest'`\n */\n align?: 'start' | 'center' | 'end' | 'nearest';\n /**\n * Scroll behavior.\n *\n * - `'instant'` — jump immediately (default)\n * - `'smooth'` — animate the scroll\n *\n * @defaultValue `'instant'`\n */\n behavior?: 'smooth' | 'instant';\n}\n// #endregion\n\n// #region PublicGrid Interface\n/**\n * Public API interface for DataGrid component.\n *\n * **Property Getters vs Setters:**\n *\n * Property getters return the EFFECTIVE (resolved) value after merging all config sources.\n * This is the \"current situation\" - what consumers and plugins need to know.\n *\n * Property setters accept input values which are merged into the effective config.\n * Multiple sources can contribute (gridConfig, columns prop, light DOM, individual props).\n *\n * For example:\n * - `grid.fitMode` returns the resolved fitMode (e.g., 'stretch' even if you set undefined)\n * - `grid.columns` returns the effective columns after merging\n * - `grid.gridConfig` returns the full effective config\n * @since 0.1.1\n */\nexport interface PublicGrid<T = any> {\n /**\n * Full config object. Setter merges with other inputs per precedence rules.\n * Getter returns the effective (resolved) config.\n */\n gridConfig?: GridConfig<T>;\n /**\n * Column definitions.\n * Getter returns effective columns (after merging config, light DOM, inference).\n */\n columns?: ColumnConfig<T>[];\n /** Current row data (after plugin processing like grouping, filtering). */\n rows?: T[];\n /** Insert a row at a visible index, bypassing the sort/filter pipeline. Auto-animates by default. */\n insertRow?(index: number, row: T, animate?: boolean): Promise<void>;\n /** Remove a row at a visible index, bypassing the sort/filter pipeline. Auto-animates by default. */\n removeRow?(index: number, animate?: boolean): Promise<T | undefined>;\n /** Apply a batch of add/update/remove mutations in a single render cycle. */\n applyTransaction?(transaction: RowTransaction<T>, animate?: boolean): Promise<TransactionResult<T>>;\n /** Batch-friendly version — merges rapid calls within a single animation frame. */\n applyTransactionAsync?(transaction: RowTransaction<T>): Promise<TransactionResult<T>>;\n /** Resolves once the component has finished initial work (layout, inference). */\n ready?: () => Promise<void>;\n /** Force a layout / measurement pass (e.g. after container resize). */\n forceLayout?: () => Promise<void>;\n /** Return effective resolved config (after inference & precedence). */\n getConfig?: () => Promise<Readonly<GridConfig<T>>>;\n /** Toggle expansion state of a group row by its generated key. */\n toggleGroup?: (key: string) => Promise<void>;\n\n // Custom Styles API\n /**\n * Register custom CSS styles to be injected into the grid.\n * Use this to style custom cell renderers, editors, or detail panels.\n * @param id - Unique identifier for the style block (for removal/updates)\n * @param css - CSS string to inject\n */\n registerStyles?: (id: string, css: string) => void;\n /**\n * Remove previously registered custom styles.\n * @param id - The ID used when registering the styles\n */\n unregisterStyles?: (id: string) => void;\n /**\n * Get list of registered custom style IDs.\n */\n getRegisteredStyles?: () => string[];\n\n // Plugin API\n /**\n * Get a plugin instance by its class.\n *\n * **Prefer {@link getPluginByName}** — it avoids importing the plugin class\n * and returns the actual registered instance with full type narrowing.\n *\n * @example\n * ```typescript\n * // Preferred: by name\n * const selection = grid.getPluginByName('selection');\n *\n * // Alternative: by class\n * const selection = grid.getPlugin(SelectionPlugin);\n * if (selection) {\n * selection.selectAll();\n * }\n * ```\n */\n getPlugin?<P>(PluginClass: new (...args: any[]) => P): P | undefined;\n /**\n * Get a plugin instance by its name.\n *\n * When a plugin augments the `PluginNameMap` interface, the return\n * type is narrowed automatically:\n *\n * ```typescript\n * const editing = grid.getPluginByName('editing');\n * editing?.beginBulkEdit(0); // ✅ typed as EditingPlugin\n * ```\n *\n * For unknown names the return type falls back to `GridPlugin | undefined`.\n */\n getPluginByName?<K extends string>(\n name: K,\n ): (K extends keyof PluginNameMap ? PluginNameMap[K] : GridPlugin) | undefined;\n\n // Shell API\n /**\n * Re-render the shell header (title, column groups, toolbar).\n * Call this after dynamically adding/removing tool panels or toolbar buttons.\n */\n refreshShellHeader?(): void;\n /**\n * Register a custom tool panel in the sidebar.\n *\n * @example\n * ```typescript\n * grid.registerToolPanel({\n * id: 'analytics',\n * title: 'Analytics',\n * icon: '📊',\n * render: (container) => {\n * container.innerHTML = '<div>Charts here...</div>';\n * }\n * });\n * ```\n */\n registerToolPanel?(panel: ToolPanelDefinition): void;\n /**\n * Unregister a previously registered tool panel.\n */\n unregisterToolPanel?(panelId: string): void;\n /**\n * Open the tool panel sidebar.\n * @param panelId - Optional ID of the section to expand on open. Takes precedence\n * over `shell.toolPanel.defaultOpen`. Falls back to default behavior with a\n * warning if the ID is not registered.\n */\n openToolPanel?(panelId?: string): void;\n /**\n * Close the tool panel sidebar.\n */\n closeToolPanel?(): void;\n /**\n * Toggle the tool panel sidebar open or closed.\n */\n toggleToolPanel?(): void;\n /**\n * Toggle an accordion section expanded or collapsed within the tool panel.\n * @param sectionId - The ID of the section to toggle\n */\n toggleToolPanelSection?(sectionId: string): void;\n\n // State Persistence API\n /**\n * Get the current column state including order, width, visibility, and sort.\n * Use for persisting user preferences to localStorage or a backend.\n *\n * @example\n * ```typescript\n * const state = grid.getColumnState();\n * localStorage.setItem('gridState', JSON.stringify(state));\n * ```\n */\n getColumnState?(): GridColumnState;\n /**\n * Read the current column state. Property-style accessor that mirrors\n * {@link PublicGrid.getColumnState}. To restore state, use\n * {@link PublicGrid.applyColumnState}.\n *\n * @example\n * ```typescript\n * const snapshot = grid.columnState;\n * ```\n */\n columnState?: GridColumnState;\n\n /**\n * Apply a previously saved column state, restoring column order, widths,\n * visibility, sort, and any plugin-contributed state. Can be called before\n * or after grid initialization — pre-init calls are deferred and applied\n * during setup.\n *\n * @example\n * ```typescript\n * const saved = localStorage.getItem('gridState');\n * if (saved) grid.applyColumnState(JSON.parse(saved));\n * ```\n */\n applyColumnState?(state: GridColumnState | undefined): void;\n\n // Sort API\n /**\n * Get the current sort state.\n *\n * Returns `null` when no sort is active.\n *\n * @example\n * ```typescript\n * const sort = grid.sortModel;\n * // { field: 'id', direction: 'desc' } | null\n * ```\n */\n readonly sortModel?: { field: string; direction: 'asc' | 'desc' } | null;\n\n /**\n * Sort by a column, toggle a column's sort direction, or clear sorting.\n *\n * - `sort('id', 'desc')` — apply sort with explicit direction\n * - `sort('id')` — toggle: none → asc → desc → none\n * - `sort(null)` — clear sort, restore original row order\n *\n * @param field - Column field to sort by, or `null` to clear\n * @param direction - Explicit direction; omit to toggle\n *\n * @example\n * ```typescript\n * grid.sort('id', 'desc'); // sort descending\n * grid.sort('price'); // toggle sort on price\n * grid.sort(null); // clear sort\n * ```\n */\n sort?(field: string | null, direction?: 'asc' | 'desc'): void;\n\n // Loading API\n /**\n * Whether the grid is currently in a loading state.\n * When true, displays a loading overlay with spinner.\n *\n * Can also be set via the `loading` HTML attribute.\n *\n * @example\n * ```typescript\n * // Show loading overlay\n * grid.loading = true;\n * const data = await fetchData();\n * grid.rows = data;\n * grid.loading = false;\n * ```\n */\n loading?: boolean;\n\n /**\n * Set loading state for a specific row.\n * Displays a small spinner indicator on the row.\n *\n * Use when persisting row data or performing row-level async operations.\n *\n * @param rowId - The row's unique identifier (from getRowId)\n * @param loading - Whether the row is loading\n *\n * @example\n * ```typescript\n * // Show loading while saving row\n * grid.setRowLoading('emp-123', true);\n * await saveRow(row);\n * grid.setRowLoading('emp-123', false);\n * ```\n */\n setRowLoading?(rowId: string, loading: boolean): void;\n\n /**\n * Set loading state for a specific cell.\n * Displays a small spinner indicator on the cell.\n *\n * Use when performing cell-level async operations (e.g., validation, lookup).\n *\n * @param rowId - The row's unique identifier (from getRowId)\n * @param field - The column field\n * @param loading - Whether the cell is loading\n *\n * @example\n * ```typescript\n * // Show loading while validating cell\n * grid.setCellLoading('emp-123', 'email', true);\n * const isValid = await validateEmail(email);\n * grid.setCellLoading('emp-123', 'email', false);\n * ```\n */\n setCellLoading?(rowId: string, field: string, loading: boolean): void;\n\n /**\n * Check if a row is currently in loading state.\n * @param rowId - The row's unique identifier\n */\n isRowLoading?(rowId: string): boolean;\n\n /**\n * Check if a cell is currently in loading state.\n * @param rowId - The row's unique identifier\n * @param field - The column field\n */\n isCellLoading?(rowId: string, field: string): boolean;\n\n /**\n * Clear all row and cell loading states.\n */\n clearAllLoading?(): void;\n\n // Focus Management API\n /**\n * Register an external DOM element as a logical focus container of this grid.\n *\n * Focus moving into a registered container is treated as if it stayed inside\n * the grid: `data-has-focus` is preserved, click-outside commit is suppressed,\n * and the editing focus trap (when enabled) won't reclaim focus.\n *\n * Typical use case: overlay panels (datepickers, dropdowns, autocompletes)\n * that render at `<body>` level to escape grid overflow clipping.\n *\n * @param el - The external element to register\n *\n * @example\n * ```typescript\n * const overlay = document.createElement('div');\n * document.body.appendChild(overlay);\n *\n * // Tell the grid this overlay is \"part of\" the grid\n * grid.registerExternalFocusContainer(overlay);\n *\n * // Later, when overlay is removed\n * grid.unregisterExternalFocusContainer(overlay);\n * ```\n */\n registerExternalFocusContainer?(el: Element): void;\n\n /**\n * Unregister a previously registered external focus container.\n *\n * @param el - The element to unregister\n */\n unregisterExternalFocusContainer?(el: Element): void;\n\n /**\n * Check whether focus is logically inside this grid.\n *\n * Returns `true` when `document.activeElement` (or the given node) is\n * inside the grid's own DOM **or** inside any element registered via\n * {@link registerExternalFocusContainer}.\n *\n * @param node - Optional node to test. Defaults to `document.activeElement`.\n *\n * @example\n * ```typescript\n * if (grid.containsFocus()) {\n * console.log('Grid or one of its overlays has focus');\n * }\n * ```\n */\n containsFocus?(node?: Node | null): boolean;\n\n // Focus & Navigation API\n /**\n * Move focus to a specific cell.\n *\n * @param rowIndex - Row index (0-based, in the current processed row array)\n * @param column - Column index (0-based into visible columns) or field name\n */\n focusCell?(rowIndex: number, column: number | string): void;\n\n /**\n * The currently focused cell position, or `null` if no rows are loaded.\n */\n readonly focusedCell?: { rowIndex: number; colIndex: number; field: string } | null;\n\n /**\n * Scroll to make a row visible by its index.\n *\n * @param rowIndex - Row index (0-based, in the current processed row array)\n * @param options - Scroll alignment and behavior\n */\n scrollToRow?(rowIndex: number, options?: ScrollToRowOptions): void;\n\n /**\n * Scroll to make a row visible by its unique ID.\n *\n * @param rowId - The row's unique identifier (from {@link GridConfig.getRowId | getRowId})\n * @param options - Scroll alignment and behavior\n */\n scrollToRowById?(rowId: string, options?: ScrollToRowOptions): void;\n}\n// #endregion\n\n// #region InternalGrid Interface\n/**\n * Internal-only augmented interface for DataGrid component.\n *\n * Member prefixes indicate accessibility:\n * - `_underscore` = protected members - private outside core, accessible to plugins. Marked with @internal.\n * - `__doubleUnderscore` = deeply internal members - private outside core, only for internal functions.\n *\n * @category Plugin Development\n * @since 0.1.1\n */\nexport interface InternalGrid<T = any> extends PublicGrid<T>, GridConfig<T> {\n // Element methods available because DataGridElement extends HTMLElement\n /** The element's `id` attribute. Available because DataGridElement extends HTMLElement. */\n id: string;\n /**\n * The grid's host HTMLElement (`this`). Use instead of casting `grid as unknown as HTMLElement`.\n * @internal\n */\n readonly _hostElement: HTMLElement;\n querySelector<K extends keyof HTMLElementTagNameMap>(selectors: K): HTMLElementTagNameMap[K] | null;\n querySelector<E extends Element = Element>(selectors: string): E | null;\n querySelectorAll<K extends keyof HTMLElementTagNameMap>(selectors: K): NodeListOf<HTMLElementTagNameMap[K]>;\n querySelectorAll<E extends Element = Element>(selectors: string): NodeListOf<E>;\n _rows: T[];\n _columns: ColumnInternal<T>[];\n /** Visible columns only (excludes hidden). Use for rendering. */\n _visibleColumns: ColumnInternal<T>[];\n _headerRowEl: HTMLElement;\n _bodyEl: HTMLElement;\n _rowPool: RowElementInternal[];\n _resizeController: ResizeController;\n _sortState: { field: string; direction: 1 | -1 } | null;\n /** Original unfiltered/unprocessed rows. @internal */\n sourceRows: T[];\n /** Framework adapter instance (set by Grid directives). @internal */\n __frameworkAdapter?: FrameworkAdapter;\n __originalOrder: T[];\n __rowRenderEpoch: number;\n __didInitialAutoSize?: boolean;\n __lightDomColumnsCache?: ColumnInternal[];\n __originalColumnNodes?: HTMLElement[];\n /** Cell display value cache. @internal */\n __cellDisplayCache?: Map<number, string[]>;\n /** Cache epoch for cell display values. @internal */\n __cellCacheEpoch?: number;\n /** Cached header row count for virtualization. @internal */\n __cachedHeaderRowCount?: number;\n /** Cached flag for whether grid has special columns (custom renderers, etc.). @internal */\n __hasSpecialColumns?: boolean;\n /** Cached flag for whether any plugin has renderRow hooks. @internal */\n __hasRenderRowPlugins?: boolean;\n /** @internal Access the plugin manager's cached state. */\n _pluginManager?: {\n _hasRowStructurePlugins: boolean;\n /** Emit an event on the plugin event bus (does not dispatch a DOM event). */\n emitPluginEvent?: <D>(eventType: string, detail: D) => void;\n /** Let plugins contribute to the effective config during merge (mutates in place). */\n processConfig?(config: GridConfig): void;\n };\n _gridTemplate: string;\n _virtualization: VirtualState;\n _focusRow: number;\n _focusCol: number;\n /** Currently active edit row index. Injected by EditingPlugin. @internal */\n _activeEditRows?: number;\n /** Whether the grid is in 'grid' editing mode (all rows editable). Injected by EditingPlugin. @internal */\n _isGridEditMode?: boolean;\n /** Snapshots of row data before editing. Injected by EditingPlugin. @internal */\n _rowEditSnapshots?: Map<number, T>;\n /** Get all changed rows. Injected by EditingPlugin. */\n changedRows?: T[];\n /** Get IDs of all changed rows. Injected by EditingPlugin. */\n changedRowIds?: string[];\n /** Internal Set for O(1) lookup in the render hot path. Injected by EditingPlugin. @internal */\n _changedRowIdSet?: ReadonlySet<string>;\n effectiveConfig?: GridConfig<T>;\n findHeaderRow?: () => HTMLElement;\n refreshVirtualWindow: (full: boolean, skipAfterRender?: boolean) => boolean;\n /** @internal Trigger a COLUMNS-phase re-render. */\n refreshColumns?: () => void;\n updateTemplate?: () => void;\n findRenderedRowElement?: (rowIndex: number) => HTMLElement | null;\n /** Get a row by its ID. Implemented in grid.ts */\n getRow?: (id: string) => T | undefined;\n /** Get a row and its current index by ID. Returns undefined if not found. @internal */\n _getRowEntry: (id: string) => { row: T; index: number } | undefined;\n /** Get the unique ID for a row. Implemented in grid.ts */\n getRowId?: (row: T) => string;\n /** Update a row by ID. Implemented in grid.ts */\n updateRow?: (id: string, changes: Partial<T>, source?: UpdateSource) => void;\n /** Animate a single row. Returns Promise that resolves when animation completes. Implemented in grid.ts */\n animateRow?: (rowIndex: number, type: RowAnimationType) => Promise<boolean>;\n /** Animate multiple rows. Returns Promise that resolves when all animations complete. Implemented in grid.ts */\n animateRows?: (rowIndices: number[], type: RowAnimationType) => Promise<number>;\n /** Animate a row by its ID. Returns Promise that resolves when animation completes. Implemented in grid.ts */\n animateRowById?: (rowId: string, type: RowAnimationType) => Promise<boolean>;\n /** Begin bulk edit on a row. Injected by EditingPlugin. */\n beginBulkEdit?: (rowIndex: number) => void;\n /** Commit active row edit. Injected by EditingPlugin. */\n commitActiveRowEdit?: () => void;\n /** Dispatch cell click to plugin system, returns true if handled */\n _dispatchCellClick?: (event: MouseEvent, rowIndex: number, colIndex: number, cellEl: HTMLElement) => boolean;\n /** Dispatch row click to plugin system, returns true if handled */\n _dispatchRowClick?: (event: MouseEvent, rowIndex: number, row: any, rowEl: HTMLElement) => boolean;\n /** Dispatch header click to plugin system, returns true if handled */\n _dispatchHeaderClick?: (event: MouseEvent | KeyboardEvent, col: ColumnConfig, headerEl: HTMLElement) => boolean;\n /** Dispatch keydown to plugin system, returns true if handled */\n _dispatchKeyDown?: (event: KeyboardEvent) => boolean;\n /** Dispatch cell mouse events for drag operations. Returns true if any plugin started a drag. */\n _dispatchCellMouseDown?: (event: CellMouseEvent) => boolean;\n /** Dispatch cell mouse move during drag. */\n _dispatchCellMouseMove?: (event: CellMouseEvent) => void;\n /** Dispatch cell mouse up to end drag. */\n _dispatchCellMouseUp?: (event: CellMouseEvent) => void;\n /** Call afterCellRender hook on all plugins. Called from rows.ts after each cell is rendered. @internal */\n _afterCellRender?: (context: AfterCellRenderContext<T>) => void;\n /** Check if any plugin has registered an afterCellRender hook. Used to skip hook call for performance. @internal */\n _hasAfterCellRenderHook?: () => boolean;\n /** Call afterRowRender hook on all plugins. Called from rows.ts after each row is rendered. @internal */\n _afterRowRender?: (context: AfterRowRenderContext<T>) => void;\n /** Check if any plugin has registered an afterRowRender hook. Used to skip hook call for performance. @internal */\n _hasAfterRowRenderHook?: () => boolean;\n /** Get horizontal scroll boundary offsets from plugins */\n _getHorizontalScrollOffsets?: (\n rowEl?: HTMLElement,\n focusedCell?: HTMLElement,\n ) => { left: number; right: number; skipScroll?: boolean };\n /** Request emission of column-state-change event (debounced) */\n requestStateChange?: () => void;\n\n // Methods exposed for extracted managers (VirtualizationManager, FocusManager, RowManager, RenderScheduler)\n /** @internal Clear the cached _visibleColumns array so the next read recomputes from _columns. */\n _invalidateVisibleColumnsCache(): void;\n /** @internal */ _renderVisibleRows(start: number, end: number, epoch?: number): void;\n /** @internal */ _updateAriaCounts(totalRows: number, totalCols: number): void;\n /** @internal */ _requestSchedulerPhase(phase: RenderPhase, source: string): void;\n /** @internal */ _rebuildRowIdMap(): void;\n /** @internal */ _emitDataChange(): void;\n /** @internal */ _getPluginRowHeight(row: T, index: number): number | undefined;\n /** @internal */ _adjustPluginVirtualStart(start: number, scrollTop: number, rowHeight: number): number | undefined;\n /** @internal */ _afterPluginRender(): void;\n /** @internal */ _emitPluginEvent(event: string, detail: unknown): void;\n\n // Scheduler pipeline callbacks\n /** @internal */ _schedulerMergeConfig(): void;\n /** @internal */ _schedulerProcessColumns(): void;\n /** @internal */ _schedulerProcessRows(): void;\n /** @internal */ _schedulerRenderHeader(): void;\n /** @internal */ _schedulerUpdateTemplate(): void;\n /** @internal */ _schedulerAfterRender(): void;\n /** @internal */ readonly _schedulerIsConnected: boolean;\n\n // Shell controller & config manager support\n /** @internal The render root element for DOM queries. */\n readonly _renderRoot: Element;\n /** @internal Emit a custom event from the grid. */\n _emit(eventName: string, detail: unknown): void;\n /** @internal Get accordion expand/collapse icons from effective config. */\n readonly _accordionIcons: { expand: IconValue; collapse: IconValue };\n /** @internal Clear the row pool and body element. */\n _clearRowPool(): void;\n /** @internal Run grid setup (DOM rebuild). */\n _setup(): void;\n /** @internal Apply animation configuration to host element. */\n _applyAnimationConfig(config: GridConfig): void;\n}\n\n/**\n * Grid reference type combining InternalGrid with HTMLElement.\n * Used by extracted managers that need both internal grid state and DOM APIs.\n * @internal\n */\nexport type GridHost<T = any> = InternalGrid<T> & HTMLElement;\n// #endregion\n\n// #region Column Types\n/**\n * Built-in primitive column types with automatic formatting and editing support.\n *\n * - `'string'` - Text content, default text input editor\n * - `'number'` - Numeric content, right-aligned, number input editor\n * - `'date'` - Date content, formatted display, date picker editor\n * - `'boolean'` - True/false, rendered as checkbox\n * - `'select'` - Dropdown selection from `options` array\n *\n * @example\n * ```typescript\n * columns: [\n * { field: 'name', type: 'string' },\n * { field: 'age', type: 'number' },\n * { field: 'hireDate', type: 'date' },\n * { field: 'active', type: 'boolean' },\n * { field: 'department', type: 'select', options: [\n * { label: 'Engineering', value: 'eng' },\n * { label: 'Sales', value: 'sales' },\n * ]},\n * ]\n * ```\n *\n * @see {@link ColumnType} for custom type support\n * @see {@link TypeDefault} for type-level defaults\n * @since 0.1.1\n */\nexport type PrimitiveColumnType = 'number' | 'string' | 'date' | 'boolean' | 'select';\n\n/**\n * Column type - built-in primitives or custom type strings.\n *\n * Use built-in types for automatic formatting, or define custom types\n * (e.g., 'currency', 'country') with type-level defaults via `typeDefaults`.\n *\n * @example\n * ```typescript\n * // Built-in types\n * { field: 'name', type: 'string' }\n * { field: 'salary', type: 'number' }\n *\n * // Custom types with defaults\n * grid.gridConfig = {\n * columns: [\n * { field: 'salary', type: 'currency' },\n * { field: 'birthCountry', type: 'country' },\n * ],\n * typeDefaults: {\n * currency: {\n * format: (v) => `$${Number(v).toFixed(2)}`,\n * },\n * country: {\n * renderer: (ctx) => `🌍 ${ctx.value}`,\n * },\n * },\n * };\n * ```\n *\n * @see {@link PrimitiveColumnType} for built-in types\n * @see {@link TypeDefault} for defining custom type defaults\n * @since 1.0.0\n */\nexport type ColumnType = PrimitiveColumnType | (string & {});\n// #endregion\n\n// #region TypeDefault Interface\n/**\n * Type-level defaults for formatters and renderers.\n * Applied to all columns of a given type unless overridden at column level.\n *\n * Note: `editor` and `editorParams` are added via module augmentation when\n * EditingPlugin is imported. `filterPanelRenderer` is added by FilteringPlugin.\n *\n * @example\n * ```typescript\n * typeDefaults: {\n * currency: {\n * format: (value) => new Intl.NumberFormat('en-US', {\n * style: 'currency',\n * currency: 'USD',\n * }).format(value as number),\n * },\n * country: {\n * renderer: (ctx) => {\n * const span = document.createElement('span');\n * span.innerHTML = `<img src=\"/flags/${ctx.value}.svg\" /> ${ctx.value}`;\n * return span;\n * }\n * }\n * }\n * ```\n *\n * @see {@link ColumnViewRenderer} for custom renderer function signature\n * @see {@link ColumnType} for type strings that can have defaults\n * @see {@link GridConfig.typeDefaults} for registering type defaults\n * @since 1.0.0\n */\nexport interface TypeDefault<TRow = unknown> {\n /**\n * Default formatter for all columns of this type.\n *\n * Transforms the raw cell value into a display string. Use when you need\n * consistent formatting across columns without custom DOM (e.g., currency,\n * percentages, dates with specific locale).\n *\n * **Resolution Priority**: Column `format` → Type `format` → Built-in\n *\n * @example\n * ```typescript\n * typeDefaults: {\n * currency: {\n * format: (value) => new Intl.NumberFormat('en-US', {\n * style: 'currency',\n * currency: 'USD',\n * }).format(value as number),\n * },\n * percentage: {\n * format: (value) => `${(value as number * 100).toFixed(1)}%`,\n * }\n * }\n * ```\n */\n format?: (value: unknown, row: TRow) => string;\n\n /**\n * Default renderer for all columns of this type.\n *\n * Creates custom DOM for the cell content. Use when you need more than\n * text formatting (e.g., icons, badges, interactive elements).\n *\n * **Resolution Priority**: Column `renderer` → Type `renderer` → App-level (adapter) → Built-in\n *\n * @example\n * ```typescript\n * typeDefaults: {\n * status: {\n * renderer: (ctx) => {\n * const badge = document.createElement('span');\n * badge.className = `badge badge-${ctx.value}`;\n * badge.textContent = ctx.value as string;\n * return badge;\n * }\n * },\n * country: {\n * renderer: (ctx) => {\n * const span = document.createElement('span');\n * span.innerHTML = `<img src=\"/flags/${ctx.value}.svg\" /> ${ctx.value}`;\n * return span;\n * }\n * }\n * }\n * ```\n */\n renderer?: ColumnViewRenderer<TRow, unknown>;\n}\n// #endregion\n\n// #region BaseColumnConfig Interface\n/**\n * Base contract for a column configuration.\n *\n * Defines the fundamental properties all columns share. Extended by {@link ColumnConfig}\n * with additional features like custom renderers and grouping.\n *\n * @example\n * ```typescript\n * // Basic column with common properties\n * const columns: BaseColumnConfig<Employee>[] = [\n * {\n * field: 'name',\n * header: 'Full Name',\n * sortable: true,\n * resizable: true,\n * },\n * {\n * field: 'salary',\n * type: 'number',\n * width: 120,\n * format: (value) => `$${value.toLocaleString()}`,\n * sortComparator: (a, b) => a - b,\n * },\n * {\n * field: 'department',\n * type: 'select',\n * options: [\n * { label: 'Engineering', value: 'eng' },\n * { label: 'Sales', value: 'sales' },\n * ],\n * },\n * ];\n * ```\n *\n * @see {@link ColumnConfig} for full column configuration with renderers\n * @see {@link ColumnType} for type options\n * @since 0.1.1\n */\nexport interface BaseColumnConfig<TRow = any, TValue = any> {\n /** Unique field key referencing property in row objects */\n field: keyof TRow & string;\n /** Visible header label; defaults to capitalized field */\n header?: string;\n /**\n * Column data type.\n *\n * Built-in types: `'string'`, `'number'`, `'date'`, `'boolean'`, `'select'`\n *\n * Custom types (e.g., `'currency'`, `'country'`) can have type-level defaults\n * via `gridConfig.typeDefaults` or framework adapter registries.\n *\n * @default Inferred from first row data\n */\n type?: ColumnType;\n /** Column width in pixels; fixed size (no flexibility) */\n width?: string | number;\n /** Minimum column width in pixels; in stretch mode uses minmax(minWidth, 1fr), and in fixed mode is used as implicit width when width is omitted */\n minWidth?: number;\n /**\n * Initial column display index.\n *\n * Sets the column's initial position in the grid declaratively. When `order` is set,\n * the column is repositioned at that index among the initial column array, _before_\n * any saved column state is applied.\n *\n * **Precedence** (low → high):\n * ```\n * data-key order (merge) < order attribute < columnState / applyColumnState / runtime reorder\n * / array order (auto)\n * ```\n *\n * If the user supplies saved column state (`gridConfig.columnState`, `applyColumnState()`,\n * or runtime reorder via drag), that state **wins**. Use `resetColumnOrder()` to return\n * to the `order`-influenced initial order.\n *\n * @example\n * ```typescript\n * // Move 'special-field' to initial index 1; other columns stay inferred\n * <tbw-grid data-src=\"/url\" column-inference=\"merge\">\n * <tbw-grid-column field=\"special-field\" order=\"1\"></tbw-grid-column>\n * </tbw-grid>\n * ```\n *\n * @see ReorderPlugin.resetColumnOrder() — returns to this order\n * @since 2.17.0\n */\n order?: number;\n /** Whether column can be sorted */\n sortable?: boolean;\n /** Whether column can be resized by user */\n resizable?: boolean;\n /** Optional custom comparator for sorting (a,b) -> number */\n sortComparator?: (a: TValue, b: TValue, rowA: TRow, rowB: TRow) => number;\n /**\n * Compute the cell's value from the row. When defined, this is the single\n * source of truth used by sorting, filtering, formatting, cell rendering,\n * export, and clipboard — eliminating the need to duplicate value-extraction\n * logic across `sortComparator`, `filterValue`, and per-renderer code.\n *\n * **Resolution precedence**:\n * - **Sort**: `sortComparator` → `valueAccessor` → `row[field]`\n * - **Filter**: `filterValue` → `valueAccessor` → `row[field]`\n * - **Render / format / export / copy**: `valueAccessor` → `row[field]`\n *\n * The accessor is the *default* value source — per-column escape hatches\n * (`sortComparator`, `filterValue`) still take precedence when set.\n *\n * Results are memoized per `(row identity, column field)` so accessors are\n * free to be \"slow but correct\" (e.g. `array.find(...)`). Immutable row\n * updates auto-invalidate; in-place mutations are invalidated by the grid's\n * edit / transaction paths.\n *\n * Note: a `valueAccessor` without a paired `valueSetter` (planned API)\n * implies the column is read-only — editors will not commit through it.\n *\n * @example\n * ```typescript\n * // Computed value from nested data\n * {\n * field: 'bolDate',\n * header: 'BL date',\n * valueAccessor: ({ row }) => {\n * if (isCargo(row)) {\n * return row.movements.find(m => m.operationType === 'LOAD')?.movementDate ?? null;\n * }\n * return row.movementDate ?? null;\n * },\n * filterType: 'date',\n * // No need for sortComparator or filterValue — they fall back to the accessor.\n * }\n * ```\n */\n valueAccessor?: (ctx: { row: TRow; column: ColumnConfig<TRow>; rowIndex: number }) => TValue;\n /** For select type - available options */\n options?: Array<{ label: string; value: unknown }> | (() => Array<{ label: string; value: unknown }>);\n /**\n * Formats the raw cell value into a display string.\n *\n * Used both for **cell rendering** and the **built-in filter panel**:\n * - In cells, the formatted value replaces the raw value as text content.\n * - In the filter panel (set filter), checkbox labels show the formatted value\n * instead of the raw value, and search matches against the formatted text.\n *\n * The `row` parameter is available during cell rendering but is `undefined`\n * when called from the filter panel (standalone value formatting). Avoid\n * accessing `row` properties in format functions intended for filter display.\n *\n * @example\n * ```typescript\n * // Currency formatter — works in both cells and filter panel\n * {\n * field: 'price',\n * format: (value) => `$${Number(value).toFixed(2)}`,\n * }\n *\n * // ID-to-name lookup — filter panel shows names, not IDs\n * {\n * field: 'departmentId',\n * format: (value) => departmentMap.get(value as string) ?? String(value),\n * }\n * ```\n */\n format?: (value: TValue, row: TRow) => string;\n /**\n * Marks this column as a **system / utility column** — a column that exists to\n * support grid behaviour rather than to display user data.\n *\n * Built-in plugins set this on the columns they synthesize (selection checkbox,\n * row-reorder drag handle, master-detail / tree / row-grouping expander). You can\n * also set it on columns you author yourself when you want them to behave the same\n * way — for example, a row-action menu column, a status indicator, or any\n * developer-defined \"system\" column that should be visible in the grid only.\n *\n * Setting `utility: true` excludes the column from:\n *\n * - **Visibility panel** — not listed in the show/hide UI\n * - **Column reorder** — header drag-drop and visibility-panel drag treat it as locked\n * (equivalent to `lockPosition: true`)\n * - **Print** — hidden by `PrintPlugin` during print\n * - **Clipboard copy** — skipped by `ClipboardPlugin`\n * - **Export** (CSV / JSON / Excel) — skipped by `ExportPlugin`\n * - **Range / row selection** — clicks land on the column but selection ignores it\n * - **Filter UI** — no filter button rendered, no filter model entry\n *\n * The column is still rendered in the grid and still receives `cellRenderer` /\n * `viewRenderer` / `headerRenderer` callbacks — it is \"hidden from the system,\n * visible in the grid\".\n *\n * Convention: name the field with a `__`-prefix (e.g. `__actions`) so it cannot\n * collide with a real data field.\n *\n * @example A custom row-actions column\n * ```ts\n * {\n * field: '__actions',\n * header: '',\n * width: 80,\n * utility: true, // excluded from print, export, reorder, visibility, etc.\n * resizable: false,\n * sortable: false,\n * filterable: false,\n * viewRenderer: ({ row }) => createActionsButton(row),\n * }\n * ```\n */\n utility?: boolean;\n /**\n * Arbitrary extra metadata for application use.\n *\n * @remarks\n * **Do not use `meta` for grid-recognized flags.** Properties like `lockPosition`,\n * `lockVisible`, `lockPinning`, `pinned`, `utility`, and `checkboxColumn` are first-class\n * augmented properties on `ColumnConfig` itself. Using `meta.<flag>` for any of them is\n * deprecated and only kept as a runtime fallback for back-compat.\n */\n meta?: Record<string, unknown>;\n}\n// #endregion\n\n// #region ColumnConfig Interface\n/**\n * Full column configuration including custom renderers, editors, and grouping metadata.\n *\n * Extends {@link BaseColumnConfig} with additional features for customizing\n * how cells are displayed and edited.\n *\n * @example\n * ```typescript\n * const columns: ColumnConfig<Employee>[] = [\n * // Basic sortable column\n * { field: 'id', header: 'ID', width: 60, sortable: true },\n *\n * // Column with custom renderer\n * {\n * field: 'name',\n * header: 'Employee',\n * renderer: (ctx) => {\n * const div = document.createElement('div');\n * div.innerHTML = `<img src=\"${ctx.row.avatar}\" /><span>${ctx.value}</span>`;\n * return div;\n * },\n * },\n *\n * // Column with custom header\n * {\n * field: 'email',\n * headerLabelRenderer: (ctx) => `${ctx.value} 📧`,\n * },\n *\n * // Editable column (requires EditingPlugin)\n * {\n * field: 'status',\n * editable: true,\n * editor: (ctx) => {\n * const select = document.createElement('select');\n * // ... editor implementation\n * return select;\n * },\n * },\n *\n * // Hidden column (can be shown via VisibilityPlugin)\n * { field: 'internalNotes', hidden: true },\n * ];\n * ```\n *\n * @see {@link BaseColumnConfig} for basic column properties\n * @see {@link ColumnViewRenderer} for custom cell renderers\n * @see {@link ColumnEditorSpec} for custom cell editors\n * @see {@link HeaderRenderer} for custom header renderers\n * @since 0.1.1\n */\nexport interface ColumnConfig<TRow = any> extends BaseColumnConfig<TRow, any> {\n /**\n * Optional custom cell renderer function. Alias for `viewRenderer`.\n * Can return an HTMLElement, a Node, or an HTML string (which will be sanitized).\n *\n * @example\n * ```typescript\n * // Simple string template\n * renderer: (ctx) => `<span class=\"badge\">${ctx.value}</span>`\n *\n * // DOM element\n * renderer: (ctx) => {\n * const el = document.createElement('span');\n * el.textContent = ctx.value;\n * return el;\n * }\n * ```\n */\n renderer?: ColumnViewRenderer<TRow, any>;\n /** Optional custom view renderer used instead of default text rendering */\n viewRenderer?: ColumnViewRenderer<TRow, any>;\n /** External view spec (lets host app mount any framework component) */\n externalView?: {\n component: unknown;\n props?: Record<string, unknown>;\n mount?: (options: {\n placeholder: HTMLElement;\n context: CellRenderContext<TRow, unknown>;\n spec: unknown;\n }) => void | { dispose?: () => void };\n };\n /** Whether the column is initially hidden */\n hidden?: boolean;\n /** Prevent this column from being hidden programmatically */\n lockVisible?: boolean;\n /**\n * Dynamic CSS class(es) for cells in this column.\n * Called for each cell during rendering. Return class names to add to the cell element.\n *\n * @example\n * ```typescript\n * // Highlight negative values\n * cellClass: (value, row, column) => value < 0 ? ['negative', 'text-red'] : []\n *\n * // Status-based styling\n * cellClass: (value) => [`status-${value}`]\n *\n * // Single class as string\n * cellClass: (value) => value < 0 ? 'negative' : ''\n * ```\n */\n cellClass?: (value: unknown, row: TRow, column: ColumnConfig<TRow>) => string | string[];\n\n /**\n * Custom header label renderer. Customize the label content while the grid\n * handles sort icons, filter buttons, resize handles, and click interactions.\n *\n * Use this for simple customizations like adding icons, badges, or units.\n *\n * @example\n * ```typescript\n * // Add required field indicator\n * headerLabelRenderer: (ctx) => `${ctx.value} <span class=\"required\">*</span>`\n *\n * // Add unit to header\n * headerLabelRenderer: (ctx) => {\n * const span = document.createElement('span');\n * span.innerHTML = `${ctx.value}<br/><small>(kg)</small>`;\n * return span;\n * }\n * ```\n */\n headerLabelRenderer?: HeaderLabelRenderer<TRow>;\n\n /**\n * Custom header cell renderer. Complete control over the entire header cell.\n * Resize handles are added automatically for resizable columns.\n *\n * The context provides helper functions to include standard elements:\n * - `renderSortIcon()` - Returns sort indicator element (null if not sortable)\n * - `renderFilterButton()` - Returns filter button (null if not filterable)\n *\n * **Precedence**: `headerRenderer` > `headerLabelRenderer` > `header` > `field`\n *\n * @example\n * ```typescript\n * headerRenderer: (ctx) => {\n * const div = document.createElement('div');\n * div.className = 'custom-header';\n * div.innerHTML = `<span>${ctx.value}</span>`;\n * const sortIcon = ctx.renderSortIcon();\n * if (sortIcon) div.appendChild(sortIcon);\n * return div;\n * }\n * ```\n */\n headerRenderer?: HeaderRenderer<TRow>;\n}\n// #endregion\n\n// #region ColumnConfigMap Type\n/**\n * Array of column configurations.\n * Convenience type alias for `ColumnConfig<TRow>[]`.\n *\n * @example\n * ```typescript\n * const columns: ColumnConfigMap<Employee> = [\n * { field: 'name', header: 'Full Name', sortable: true },\n * { field: 'email', header: 'Email Address' },\n * { field: 'department', type: 'select', options: deptOptions },\n * ];\n *\n * grid.columns = columns;\n * ```\n *\n * @see {@link ColumnConfig} for individual column options\n * @see {@link GridConfig.columns} for setting columns on the grid\n * @since 0.1.1\n */\nexport type ColumnConfigMap<TRow = any> = ColumnConfig<TRow>[];\n// #endregion\n\n// #region Editor Types\n/**\n * Editor specification for inline cell editing.\n * Supports multiple formats for maximum flexibility.\n *\n * **Format Options:**\n * - `string` - Custom element tag name (e.g., 'my-date-picker')\n * - `function` - Factory function returning an editor element\n * - `object` - External component spec for framework integration\n *\n * @example\n * ```typescript\n * // 1. Custom element tag name\n * columns: [\n * { field: 'date', editor: 'my-date-picker' }\n * ]\n *\n * // 2. Factory function (full control)\n * columns: [\n * {\n * field: 'status',\n * editor: (ctx) => {\n * const select = document.createElement('select');\n * select.innerHTML = `\n * <option value=\"active\">Active</option>\n * <option value=\"inactive\">Inactive</option>\n * `;\n * select.value = ctx.value;\n * select.onchange = () => ctx.commit(select.value);\n * select.onkeydown = (e) => {\n * if (e.key === 'Escape') ctx.cancel();\n * };\n * return select;\n * }\n * }\n * ]\n *\n * // 3. External component (React, Angular, Vue)\n * columns: [\n * {\n * field: 'country',\n * editor: {\n * component: CountrySelect,\n * props: { showFlags: true }\n * }\n * }\n * ]\n * ```\n *\n * @see {@link ColumnEditorContext} for the context passed to factory functions\n * @since 0.1.1\n */\nexport type ColumnEditorSpec<TRow = unknown, TValue = unknown> =\n | string // custom element tag name\n | ((context: ColumnEditorContext<TRow, TValue>) => HTMLElement | string)\n | {\n /** Arbitrary component reference (class, function, token) */\n component: unknown;\n /** Optional static props passed to mount */\n props?: Record<string, unknown>;\n /** Optional custom mount function; if provided we call it directly instead of emitting an event */\n mount?: (options: {\n placeholder: HTMLElement;\n context: ColumnEditorContext<TRow, TValue>;\n spec: unknown;\n }) => void | { dispose?: () => void };\n };\n\n/**\n * Context object provided to editor factories allowing mutation (commit/cancel) of a cell value.\n *\n * The `commit` and `cancel` functions control the editing lifecycle:\n * - Call `commit(newValue)` to save changes and exit edit mode\n * - Call `cancel()` to discard changes and exit edit mode\n *\n * @example\n * ```typescript\n * const myEditor: ColumnEditorSpec = (ctx: ColumnEditorContext) => {\n * const input = document.createElement('input');\n * input.value = ctx.value;\n * input.className = 'my-editor';\n *\n * // Save on Enter, cancel on Escape\n * input.onkeydown = (e) => {\n * if (e.key === 'Enter') {\n * ctx.commit(input.value);\n * } else if (e.key === 'Escape') {\n * ctx.cancel();\n * }\n * };\n *\n * // Access row data for validation\n * if (ctx.row.locked) {\n * input.disabled = true;\n * }\n *\n * return input;\n * };\n * ```\n *\n * @see {@link ColumnEditorSpec} for editor specification options\n * @since 0.1.1\n */\nexport interface ColumnEditorContext<TRow = any, TValue = any> {\n /** Underlying full row object for the active edit. */\n row: TRow;\n /** Current cell value (mutable only via commit). */\n value: TValue;\n /** Field name being edited. */\n field: keyof TRow & string;\n /** Column configuration reference. */\n column: ColumnConfig<TRow>;\n /**\n * Stable row identifier (from `getRowId`).\n * Empty string if no `getRowId` is configured.\n */\n rowId: string;\n /** Accept the edit; triggers change tracking + rerender. */\n commit: (newValue: TValue) => void;\n /** Abort edit without persisting changes. */\n cancel: () => void;\n /**\n * Update other fields in this row while the editor is open.\n * Changes are committed with source `'cascade'`, triggering\n * `cell-change` events and `onValueChange` pushes to sibling editors.\n *\n * Useful for editors that affect multiple fields (e.g., an address\n * lookup that sets city + zip + state).\n *\n * @example\n * ```typescript\n * // In a cell-commit listener:\n * grid.on('cell-commit', (detail) => {\n * if (detail.field === 'quantity') {\n * detail.updateRow({ total: detail.row.price * detail.value });\n * }\n * });\n * ```\n */\n updateRow: (changes: Partial<TRow>) => void;\n /**\n * Register a callback invoked when the cell's underlying value changes\n * while the editor is open (e.g., via `updateRow()` from another cell's commit).\n *\n * Built-in editors auto-update their input values. Custom/framework editors\n * should use this to stay in sync with external mutations.\n *\n * @example\n * ```typescript\n * const editor = (ctx: ColumnEditorContext) => {\n * const input = document.createElement('input');\n * input.value = String(ctx.value);\n * ctx.onValueChange?.((newValue) => {\n * input.value = String(newValue);\n * });\n * return input;\n * };\n * ```\n */\n onValueChange?: (callback: (newValue: TValue) => void) => void;\n /**\n * The grid element that owns this editor.\n *\n * Use to access public grid API from custom editors — e.g.\n * `grid.registerExternalFocusContainer(panel)` so the grid treats focus\n * inside a portal-rendered overlay (Autocomplete, date picker, color\n * picker) as \"still inside the editor\" and does not exit row edit on\n * click. Mirrors {@link CellRenderContext.grid} for renderers.\n *\n * Always populated when the editor is mounted via the grid's editing\n * pipeline. Optional in the type for backwards compatibility with\n * factory functions written against the original signature.\n *\n * @example\n * ```typescript\n * const editor: ColumnEditorSpec = (ctx: ColumnEditorContext) => {\n * const panel = document.createElement('div');\n * document.body.appendChild(panel);\n * ctx.grid?.registerExternalFocusContainer(panel);\n * // ...\n * };\n * ```\n *\n * @see {@link CellRenderContext.grid}\n */\n grid?: PublicGrid<TRow> & HTMLElement;\n}\n// #endregion\n\n// #region Renderer Types\n/**\n * Context passed to custom view renderers (pure display – no commit helpers).\n *\n * Used by `viewRenderer` and `renderer` column properties to create\n * custom cell content. Return a DOM node or HTML string.\n *\n * @example\n * ```typescript\n * // Status badge renderer\n * const statusRenderer: ColumnViewRenderer = (ctx: CellRenderContext) => {\n * const badge = document.createElement('span');\n * badge.className = `badge badge-${ctx.value}`;\n * badge.textContent = ctx.value;\n * return badge;\n * };\n *\n * // Progress bar using row data\n * const progressRenderer: ColumnViewRenderer = (ctx) => {\n * const bar = document.createElement('div');\n * bar.className = 'progress-bar';\n * bar.style.width = `${ctx.value}%`;\n * bar.title = `${ctx.row.taskName}: ${ctx.value}%`;\n * return bar;\n * };\n *\n * // Return HTML string (simpler, less performant)\n * const htmlRenderer: ColumnViewRenderer = (ctx) => {\n * return `<strong>${ctx.value}</strong>`;\n * };\n * ```\n *\n * @see {@link ColumnViewRenderer} for the renderer function signature\n * @since 0.1.1\n */\nexport interface CellRenderContext<TRow = any, TValue = any> {\n /** Row object for the cell being rendered. */\n row: TRow;\n /** Value at field. */\n value: TValue;\n /** Field key. */\n field: keyof TRow & string;\n /** Column configuration reference. */\n column: ColumnConfig<TRow>;\n /**\n * The grid element that owns this cell.\n * Use to access public grid API (e.g., `getPluginByName()`) from custom renderers.\n *\n * @example\n * ```typescript\n * const renderer: ColumnViewRenderer<MyRow> = (ctx) => {\n * const tree = ctx.grid?.getPluginByName('tree');\n * // ...\n * };\n * ```\n */\n grid?: PublicGrid<TRow> & HTMLElement;\n /**\n * The cell DOM element being rendered into.\n * Framework adapters can use this to cache per-cell state (e.g., React roots).\n * @internal\n */\n cellEl?: HTMLElement;\n}\n\n/**\n * Custom view renderer function for cell content.\n *\n * Returns one of:\n * - `Node` - DOM element to display in the cell\n * - `string` - HTML string (parsed and inserted)\n * - `void | null` - Use default text rendering\n *\n * @example\n * ```typescript\n * // DOM element (recommended for interactivity)\n * const avatarRenderer: ColumnViewRenderer<Employee> = (ctx) => {\n * const img = document.createElement('img');\n * img.src = ctx.row.avatarUrl;\n * img.alt = ctx.row.name;\n * img.className = 'avatar';\n * return img;\n * };\n *\n * // HTML string (simpler, good for static content)\n * const emailRenderer: ColumnViewRenderer = (ctx) => {\n * return `<a href=\"mailto:${ctx.value}\">${ctx.value}</a>`;\n * };\n *\n * // Conditional rendering\n * const conditionalRenderer: ColumnViewRenderer = (ctx) => {\n * if (!ctx.value) return null; // Use default\n * return `<em>${ctx.value}</em>`;\n * };\n * ```\n *\n * @see {@link CellRenderContext} for available context properties\n * @since 0.1.1\n */\nexport type ColumnViewRenderer<TRow = unknown, TValue = unknown> = (\n ctx: CellRenderContext<TRow, TValue>,\n) => Node | string | void | null;\n// #endregion\n\n// #region Header Renderer Types\n/**\n * Context passed to `headerLabelRenderer` for customizing header label content.\n * The framework handles sort icons, filter buttons, resize handles, and click interactions.\n *\n * @example\n * ```typescript\n * headerLabelRenderer: (ctx) => {\n * const span = document.createElement('span');\n * span.innerHTML = `${ctx.value} <span class=\"required\">*</span>`;\n * return span;\n * }\n * ```\n * @since 1.4.0\n */\nexport interface HeaderLabelContext<TRow = unknown> {\n /** Column configuration reference. */\n column: ColumnConfig<TRow>;\n /** The header text (from column.header or column.field). */\n value: string;\n}\n\n/**\n * Context passed to `headerRenderer` for complete control over header cell content.\n * When using this, you control the header content. Resize handles are added automatically\n * for resizable columns.\n *\n * @example\n * ```typescript\n * headerRenderer: (ctx) => {\n * const div = document.createElement('div');\n * div.className = 'custom-header';\n * div.innerHTML = `<span>${ctx.value}</span>`;\n * // Optionally include sort icon\n * const sortIcon = ctx.renderSortIcon();\n * if (sortIcon) div.appendChild(sortIcon);\n * return div;\n * }\n * ```\n * @since 1.4.0\n */\nexport interface HeaderCellContext<TRow = unknown> {\n /** Column configuration reference. */\n column: ColumnConfig<TRow>;\n /** The header text (from column.header or column.field). */\n value: string;\n /** Current sort state for this column. */\n sortState: 'asc' | 'desc' | null;\n /** Whether the column has an active filter. */\n filterActive: boolean;\n /** The header cell DOM element being rendered into. */\n cellEl: HTMLElement;\n /**\n * Render the standard sort indicator icon.\n * Returns null if column is not sortable.\n */\n renderSortIcon: () => HTMLElement | null;\n /**\n * Render the standard filter button.\n * Returns null if FilteringPlugin is not active or column is not filterable.\n * Note: The actual button is added by FilteringPlugin's afterRender hook.\n */\n renderFilterButton: () => HTMLElement | null;\n}\n\n/**\n * Header label renderer function type.\n * Customize the label while framework handles sort icons, filter buttons, resize handles.\n *\n * Use this for simple label customizations without taking over the entire header.\n * The grid automatically appends sort icons, filter buttons, and resize handles.\n *\n * @example\n * ```typescript\n * // Add required indicator\n * const requiredHeader: HeaderLabelRenderer = (ctx) => {\n * return `${ctx.value} <span style=\"color: red;\">*</span>`;\n * };\n *\n * // Add unit suffix\n * const priceHeader: HeaderLabelRenderer = (ctx) => {\n * const span = document.createElement('span');\n * span.innerHTML = `${ctx.value} <small>(USD)</small>`;\n * return span;\n * };\n *\n * // Column config usage\n * columns: [\n * { field: 'name', headerLabelRenderer: requiredHeader },\n * { field: 'price', headerLabelRenderer: priceHeader },\n * ]\n * ```\n *\n * @see {@link HeaderLabelContext} for context properties\n * @see {@link HeaderRenderer} for full header control\n * @since 1.4.0\n */\nexport type HeaderLabelRenderer<TRow = unknown> = (ctx: HeaderLabelContext<TRow>) => Node | string | void | null;\n\n/**\n * Header cell renderer function type.\n * Full control over header cell content. User is responsible for all content and interactions.\n *\n * When using this, you have complete control but must manually include\n * sort icons, filter buttons, and resize handles using the helper functions.\n *\n * @example\n * ```typescript\n * // Custom header with all standard elements\n * const customHeader: HeaderRenderer = (ctx) => {\n * const div = document.createElement('div');\n * div.className = 'custom-header';\n * div.innerHTML = `<span class=\"label\">${ctx.value}</span>`;\n *\n * // Add sort icon (returns null if not sortable)\n * const sortIcon = ctx.renderSortIcon();\n * if (sortIcon) div.appendChild(sortIcon);\n *\n * // Add filter button (returns null if not filterable)\n * const filterBtn = ctx.renderFilterButton();\n * if (filterBtn) div.appendChild(filterBtn);\n *\n * // Resize handles are added automatically for resizable columns\n * return div;\n * };\n *\n * // Minimal header (no sort/resize)\n * const minimalHeader: HeaderRenderer = (ctx) => {\n * return `<div class=\"minimal\">${ctx.value}</div>`;\n * };\n *\n * // Column config usage\n * columns: [\n * { field: 'name', headerRenderer: customHeader },\n * ]\n * ```\n *\n * @see {@link HeaderCellContext} for context properties and helper functions\n * @see {@link HeaderLabelRenderer} for simpler label-only customization\n * @since 1.4.0\n */\nexport type HeaderRenderer<TRow = unknown> = (ctx: HeaderCellContext<TRow>) => Node | string | void | null;\n// #endregion\n\n// #region Framework Adapter Interface\n/**\n * Framework adapter interface for handling framework-specific component instantiation.\n * Allows framework libraries (Angular, React, Vue) to register handlers that convert\n * declarative light DOM elements into functional renderers/editors.\n *\n * @example\n * ```typescript\n * // In @toolbox-web/grid-angular\n * class AngularGridAdapter implements FrameworkAdapter {\n * canHandle(element: HTMLElement): boolean {\n * return element.tagName.startsWith('APP-');\n * }\n * createRenderer(element: HTMLElement): ColumnViewRenderer {\n * return (ctx) => {\n * // Angular-specific instantiation logic\n * const componentRef = createComponent(...);\n * componentRef.setInput('value', ctx.value);\n * return componentRef.location.nativeElement;\n * };\n * }\n * createEditor(element: HTMLElement): ColumnEditorSpec {\n * return (ctx) => {\n * // Angular-specific editor with commit/cancel\n * const componentRef = createComponent(...);\n * componentRef.setInput('value', ctx.value);\n * // Subscribe to commit/cancel outputs\n * return componentRef.location.nativeElement;\n * };\n * }\n * }\n *\n * // User registers adapter once in their app\n * GridElement.registerAdapter(new AngularGridAdapter(injector, appRef));\n * ```\n * @category Framework Adapters\n * @since 0.2.9\n */\nexport interface FrameworkAdapter {\n /**\n * Determines if this adapter can handle the given element.\n * Typically checks tag name, attributes, or other conventions.\n */\n canHandle(element: HTMLElement): boolean;\n\n /**\n * Creates a view renderer function from a light DOM element.\n * The renderer receives cell context and returns DOM or string.\n * Returns undefined if no renderer template is registered, allowing the grid\n * to use its default rendering.\n */\n createRenderer<TRow = unknown, TValue = unknown>(element: HTMLElement): ColumnViewRenderer<TRow, TValue> | undefined;\n\n /**\n * Creates an editor spec from a light DOM element.\n * The editor receives context with commit/cancel and returns DOM.\n * Returns undefined if no editor template is registered, allowing the grid\n * to use its default built-in editors.\n */\n createEditor<TRow = unknown, TValue = unknown>(element: HTMLElement): ColumnEditorSpec<TRow, TValue> | undefined;\n\n /**\n * Creates a header cell renderer from a light DOM element (e.g. a slot or\n * template on a `<tbw-grid-column>`). Receives `HeaderCellContext` and\n * returns DOM / string for the entire header cell — the user is\n * responsible for sort icons and filter buttons (use\n * `ctx.renderSortIcon()` / `ctx.renderFilterButton()` helpers).\n *\n * Resize handles are appended automatically by the grid for resizable\n * columns regardless of which renderer path is active; do not render one\n * yourself.\n *\n * Returns undefined when the adapter has no header renderer registered\n * for this element, letting the grid fall back to its built-in header.\n *\n * Mirrors `headerRenderer` from `BaseColumnConfig`. Optional on the\n * adapter — adapters that don't expose a slot/template surface for\n * full-header customization can omit it; callers must null-check.\n *\n * @since 2.15.0\n */\n createHeaderRenderer?<TRow = unknown>(element: HTMLElement): HeaderRenderer<TRow> | undefined;\n\n /**\n * Creates a header *label* renderer from a light DOM element. The grid\n * keeps ownership of the sort icon, filter button, and resize handle;\n * the returned function only customizes the label content.\n *\n * Returns undefined when the adapter has no header label renderer\n * registered for this element.\n *\n * Mirrors `headerLabelRenderer` from `BaseColumnConfig`. Optional on\n * the adapter — see `createHeaderRenderer` for rationale.\n *\n * @since 2.15.0\n */\n createHeaderLabelRenderer?<TRow = unknown>(element: HTMLElement): HeaderLabelRenderer<TRow> | undefined;\n\n /**\n * Creates a tool panel renderer from a light DOM element.\n * The renderer receives a container element and optionally returns a cleanup function.\n */\n createToolPanelRenderer?(element: HTMLElement): ((container: HTMLElement) => void | (() => void)) | undefined;\n\n /**\n * Gets type-level defaults from an application-level registry.\n * Used by Angular's `GridTypeRegistry` and React's `GridTypeProvider`.\n *\n * @param type - The column type (e.g., 'date', 'currency', 'country')\n * @param gridEl - The owning `<tbw-grid>` element. Helps adapters resolve\n * the correct context provider in multi-grid scenarios.\n * @returns Type defaults for renderer/editor, or undefined if not registered\n */\n getTypeDefault?<TRow = unknown>(type: string, gridEl?: HTMLElement): TypeDefault<TRow> | undefined;\n\n /**\n * Pre-process a grid config before the grid core applies it.\n * Framework adapters use this to convert framework-specific component references\n * (Angular classes, Vue components, React elements) to DOM-returning functions.\n *\n * Called automatically by the grid's `set gridConfig` setter when a\n * `__frameworkAdapter` is present on the grid instance.\n *\n * Must be **idempotent** — already-processed configs must pass through safely.\n *\n * @param config - The raw grid config (may contain framework-specific values)\n * @returns Processed config with DOM-returning functions\n */\n processConfig?<TRow = unknown>(config: GridConfig<TRow>): GridConfig<TRow>;\n\n /**\n * Called when a cell's content is about to be wiped (e.g., when exiting edit mode,\n * scroll-recycling a row, or rebuilding a row).\n *\n * Framework adapters should use this to properly destroy cached views/components\n * associated with the cell to prevent memory leaks.\n *\n * @param cellEl - The cell element whose content is being released\n */\n releaseCell?(cellEl: HTMLElement): void;\n\n /**\n * Open a teardown batch. Grid core wraps multi-cell teardown sequences\n * (e.g., `_clearRowPool`, row-pool shrink, full row rebuild) where every\n * affected cell will be detached from the DOM before the batch ends.\n *\n * Adapters that normally synchronously commit framework teardown per\n * `releaseCell` (React's `flushSync`) should defer those commits until\n * the matching {@link endBatch} call. Detached containers can then be\n * pruned without emitting per-cell render warnings.\n *\n * Calls may nest; adapters MUST track depth and only flush on the\n * outermost {@link endBatch}.\n *\n * @param gridEl - The grid element whose adapter-managed cells are\n * being torn down. Adapters that key state per grid (e.g. one\n * PortalManager per grid) should scope the batch to this element.\n * Omitted only by callers without a grid reference.\n *\n * @since 2.14.0\n */\n beginBatch?(gridEl?: HTMLElement): void;\n\n /**\n * Close a teardown batch opened by {@link beginBatch}. Adapters should\n * flush any deferred framework commits here (or rely on render-time\n * detached-container filtering for adapters that don't need a flush).\n *\n * @param gridEl - Must match the element passed to the paired\n * {@link beginBatch} call.\n *\n * @since 2.14.0\n */\n endBatch?(gridEl?: HTMLElement): void;\n\n /**\n * Unmount a specific framework container and free its resources.\n *\n * Called by the grid core (e.g., MasterDetailPlugin) when a container\n * created by the adapter is about to be removed from the DOM.\n * The adapter should destroy the associated framework instance\n * (React root, Vue app, Angular view) and remove it from tracking arrays.\n *\n * @param container - The container element returned by a create* method\n */\n unmount?(container: HTMLElement): void;\n\n /**\n * Parse a `<tbw-grid-detail>` element and return a detail renderer function.\n * Used by MasterDetailPlugin to support framework-specific detail templates.\n */\n parseDetailElement?<TRow = unknown>(\n element: Element,\n ): ((row: TRow, rowIndex: number) => HTMLElement | string) | undefined;\n\n /**\n * Parse a `<tbw-grid-responsive-card>` element and return a card renderer function.\n * Used by ResponsivePlugin to support framework-specific card templates.\n */\n parseResponsiveCardElement?<TRow = unknown>(\n element: Element,\n ): ((row: TRow, rowIndex: number) => HTMLElement) | undefined;\n}\n// #endregion\n\n// #region Internal Types\n\n/**\n * Extended column config used internally.\n * Includes all internal properties needed during grid lifecycle.\n *\n * Plugin developers may need to access these when working with\n * column caching and compiled templates.\n *\n * @example\n * ```typescript\n * import type { ColumnInternal } from '@toolbox-web/grid';\n *\n * class MyPlugin extends BaseGridPlugin {\n * afterRender(): void {\n * // Access internal column properties\n * const columns = this.columns as ColumnInternal[];\n * for (const col of columns) {\n * // Check if column was auto-sized\n * if (col.__autoSized) {\n * console.log(`${col.field} was auto-sized`);\n * }\n * }\n * }\n * }\n * ```\n *\n * @see {@link ColumnConfig} for public column properties\n * @category Plugin Development\n * @internal\n * @since 0.1.1\n */\nexport interface ColumnInternal<T = any> extends ColumnConfig<T> {\n __autoSized?: boolean;\n __userResized?: boolean;\n __renderedWidth?: number;\n /** Original configured width (for reset on double-click) */\n __originalWidth?: number;\n __viewTemplate?: HTMLElement;\n __editorTemplate?: HTMLElement;\n __headerTemplate?: HTMLElement;\n __compiledView?: CompiledViewFunction<T>;\n __compiledEditor?: (ctx: EditorExecContext<T>) => string;\n /**\n * The originating `<tbw-grid-column>` light-DOM element, if this column was\n * parsed from declarative HTML. Plugins read their own attributes from this\n * element inside their `processColumns` hook (see issue #272). Core only\n * parses a minimal set of structural attributes; everything else is\n * plugin-owned.\n * @since 2.16.0\n */\n __element?: HTMLElement;\n}\n\n/**\n * Row element with internal tracking properties.\n * Used during virtualization and row pooling.\n *\n * @category Plugin Development\n * @internal\n * @since 0.4.0\n */\nexport interface RowElementInternal extends HTMLElement {\n /** Epoch marker for row render invalidation */\n __epoch?: number;\n /** Reference to the row data object for change detection */\n __rowDataRef?: unknown;\n /** Count of cells currently in edit mode */\n __editingCellCount?: number;\n /** Flag indicating this is a custom-rendered row (group row, etc.) */\n __isCustomRow?: boolean;\n}\n\n/**\n * Type-safe access to element.part API (DOMTokenList-like).\n * Used for CSS ::part styling support.\n * @internal\n */\nexport interface ElementWithPart {\n part?: DOMTokenList;\n}\n\n/**\n * Compiled view function type with optional blocked flag.\n * The __blocked flag is set when a template contains unsafe expressions.\n *\n * @category Plugin Development\n * @internal\n * @since 0.4.0\n */\nexport interface CompiledViewFunction<T = any> {\n (ctx: CellContext<T>): string;\n /** Set to true when template was blocked due to unsafe expressions */\n __blocked?: boolean;\n}\n\n/**\n * Runtime cell context used internally for compiled template execution.\n *\n * Contains the minimal context needed to render a cell: the row data,\n * cell value, field name, and column configuration.\n *\n * @example\n * ```typescript\n * import type { CellContext, ColumnInternal } from '@toolbox-web/grid';\n *\n * // Used internally by compiled templates\n * const renderCell = (ctx: CellContext) => {\n * return `<span title=\"${ctx.field}\">${ctx.value}</span>`;\n * };\n * ```\n *\n * @see {@link CellRenderContext} for public cell render context\n * @see {@link EditorExecContext} for editor context with commit/cancel\n * @category Plugin Development\n * @since 0.1.1\n */\nexport interface CellContext<T = any> {\n row: T;\n value: unknown;\n field: string;\n column: ColumnInternal<T>;\n}\n\n/**\n * Internal editor execution context extending the generic cell context with commit helpers.\n *\n * Used internally by the editing system. For public editor APIs,\n * prefer using {@link ColumnEditorContext}.\n *\n * @example\n * ```typescript\n * import type { EditorExecContext } from '@toolbox-web/grid';\n *\n * // Internal editor template execution\n * const execEditor = (ctx: EditorExecContext) => {\n * const input = document.createElement('input');\n * input.value = String(ctx.value);\n * input.onkeydown = (e) => {\n * if (e.key === 'Enter') ctx.commit(input.value);\n * if (e.key === 'Escape') ctx.cancel();\n * };\n * return input;\n * };\n * ```\n *\n * @see {@link ColumnEditorContext} for public editor context\n * @see {@link CellContext} for base cell context\n * @category Plugin Development\n * @since 0.1.1\n */\nexport interface EditorExecContext<T = any> extends CellContext<T> {\n commit: (newValue: unknown) => void;\n cancel: () => void;\n}\n\n/**\n * Controller managing drag-based column resize lifecycle.\n *\n * Exposed internally for plugins that need to interact with resize behavior.\n *\n * @example\n * ```typescript\n * import type { ResizeController, InternalGrid } from '@toolbox-web/grid';\n *\n * class MyPlugin extends BaseGridPlugin {\n * handleColumnAction(colIndex: number): void {\n * const grid = this.grid as InternalGrid;\n * const resizeCtrl = grid._resizeController;\n *\n * // Check if resize is in progress\n * if (resizeCtrl?.isResizing) {\n * return; // Don't interfere\n * }\n *\n * // Reset column to configured width\n * resizeCtrl?.resetColumn(colIndex);\n * }\n * }\n * ```\n *\n * @see {@link ColumnResizeDetail} for resize event details\n * @category Plugin Development\n * @since 0.1.1\n */\nexport interface ResizeController {\n start: (e: MouseEvent, colIndex: number, cell: HTMLElement) => void;\n /** Reset a column to its configured width (or auto-size if none configured). */\n resetColumn: (colIndex: number) => void;\n dispose: () => void;\n /** True while a resize drag is in progress (used to suppress header click/sort). */\n isResizing: boolean;\n}\n\n/**\n * Virtual window bookkeeping; modified in-place as scroll position changes.\n *\n * Tracks virtualization state for row rendering. The grid only renders\n * rows within the visible viewport window (start to end) plus overscan.\n *\n * @example\n * ```typescript\n * import type { VirtualState, InternalGrid } from '@toolbox-web/grid';\n *\n * class MyPlugin extends BaseGridPlugin {\n * logVirtualWindow(): void {\n * const grid = this.grid as InternalGrid;\n * const vs = grid.virtualization;\n *\n * console.log(`Row height: ${vs.rowHeight}px`);\n * console.log(`Visible rows: ${vs.start} to ${vs.end}`);\n * console.log(`Virtualization: ${vs.enabled ? 'on' : 'off'}`);\n * }\n * }\n * ```\n *\n * @see {@link GridConfig.rowHeight} for configuring row height\n * @category Plugin Development\n * @since 0.1.1\n */\nexport interface VirtualState {\n enabled: boolean;\n rowHeight: number;\n /** Threshold for bypassing virtualization (renders all rows if totalRows <= bypassThreshold) */\n bypassThreshold: number;\n start: number;\n end: number;\n /** Faux scrollbar element that provides scroll events (AG Grid pattern) */\n container: HTMLElement | null;\n /** Rows viewport element for measuring visible area height */\n viewportEl: HTMLElement | null;\n /** Spacer element inside faux scrollbar for setting virtual height */\n totalHeightEl: HTMLElement | null;\n\n // --- Variable Row Height Support (Phase 1) ---\n\n /**\n * Position cache for variable row heights.\n * Index-based array mapping row index → {offset, height, measured}.\n * Rebuilt when row count changes (expand/collapse, filter).\n * `null` when using uniform row heights (default).\n */\n positionCache: RowPositionEntry[] | null;\n\n /**\n * Height cache by row identity.\n * Persists row heights across position cache rebuilds.\n * Uses dual storage: Map for string keys (rowId, __rowCacheKey) and WeakMap for object refs.\n */\n heightCache: {\n /** Heights keyed by string (synthetic rows with __rowCacheKey, or rowId-keyed rows) */\n byKey: Map<string, number>;\n /** Heights keyed by object reference (data rows without rowId) */\n byRef: WeakMap<object, number>;\n };\n\n /** Running average of measured row heights. Used for estimating unmeasured rows. */\n averageHeight: number;\n\n /** Number of rows that have been measured. */\n measuredCount: number;\n\n /** Whether variable row height mode is active (rowHeight is a function). */\n variableHeights: boolean;\n\n // --- Cached Geometry (avoid forced layout reads on scroll hot path) ---\n\n /** Cached viewport element height. Updated by ResizeObserver and force-refresh only. */\n cachedViewportHeight: number;\n\n /** Cached faux scrollbar element height. Updated alongside viewport height. */\n cachedFauxHeight: number;\n\n /** Cached scroll-area element height. Updated alongside viewport/faux heights. */\n cachedScrollAreaHeight: number;\n\n /** Cached reference to .tbw-scroll-area element. Set during scroll listener setup. */\n scrollAreaEl: HTMLElement | null;\n\n /**\n * Active scroll mapping between native `scrollTop` (clamped spacer space) and\n * \"virtual\" row-content space. Identity (`capped: false`) for datasets within\n * the browser's max-element-height cap (Chromium ~33.5M px). For larger datasets,\n * the spacer height is clamped and `scrollTop` must be translated via this\n * mapping before computing the visible window. Updated by `calculateTotalSpacerHeight`.\n *\n * @see {@link computeScrollMapping}\n * @since 2.13.0\n */\n scrollMapping: ScrollMapping;\n}\n\n// RowElementInternal is now defined earlier in the file with all internal properties\n\n/**\n * Union type for input-like elements that have a `value` property.\n * Covers standard form elements and custom elements with value semantics.\n *\n * @category Plugin Development\n * @internal\n * @since 0.4.0\n */\nexport type InputLikeElement = HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement | { value: unknown };\n// #endregion\n\n// #region Grouping & Footer Public Types\n/**\n * Group row rendering customization options.\n * Controls how group header rows are displayed in the GroupingRowsPlugin.\n *\n * @example\n * ```typescript\n * import { GroupingRowsPlugin } from '@toolbox-web/grid/all';\n *\n * new GroupingRowsPlugin({\n * groupOn: (row) => [row.department, row.team],\n * render: {\n * // Group row spans all columns\n * fullWidth: true,\n *\n * // Custom label format\n * formatLabel: (value, depth, key) => {\n * if (depth === 0) return `Department: ${value}`;\n * return `Team: ${value}`;\n * },\n *\n * // Show aggregates in group rows (when not fullWidth)\n * aggregators: {\n * salary: 'sum',\n * age: 'avg',\n * },\n *\n * // Custom CSS class\n * class: 'my-group-row',\n * },\n * });\n * ```\n *\n * @see {@link AggregatorRef} for aggregation options\n * @since 0.1.1\n */\nexport interface RowGroupRenderConfig {\n /** If true, group rows span all columns (single full-width cell). Default false. */\n fullWidth?: boolean;\n /** Optional label formatter override. Receives raw group value + depth. */\n formatLabel?: (value: unknown, depth: number, key: string) => string;\n /** Optional aggregate overrides per field for group summary cells (only when not fullWidth). */\n aggregators?: Record<string, AggregatorRef>;\n /** Additional CSS class applied to each group row root element. */\n class?: string;\n}\n\n/**\n * Reference to an aggregation function for footer/group summaries.\n *\n * Can be either:\n * - A built-in aggregator name: `'sum'`, `'avg'`, `'min'`, `'max'`, `'count'`\n * - A custom function that calculates the aggregate value\n *\n * Aggregators are not declared on a column directly — they are configured per\n * field on the consumer (e.g. {@link RowGroupRenderConfig.aggregators} for\n * group rows, keyed by column field name).\n *\n * @example\n * ```typescript\n * // Built-in aggregator on a group row config\n * { aggregators: { amount: 'sum' } }\n *\n * // Custom aggregator function\n * {\n * aggregators: {\n * price: (rows, field) => {\n * const values = rows.map((r) => (r as any)[field]).filter((v) => v != null);\n * return values.length ? Math.max(...values as number[]) : null;\n * },\n * },\n * }\n * ```\n *\n * @see {@link RowGroupRenderConfig} for using aggregators in group rows\n * @since 0.1.1\n */\nexport type AggregatorRef = string | ((rows: unknown[], field: string, column?: unknown) => unknown);\n\n/**\n * Result of automatic column inference from sample rows.\n *\n * When no columns are configured, the grid analyzes the first row of data\n * to automatically generate column definitions with inferred types.\n *\n * @example\n * ```typescript\n * // Automatic inference (no columns configured)\n * grid.rows = [\n * { name: 'Alice', age: 30, active: true, hireDate: new Date() },\n * ];\n * // Grid infers:\n * // - name: type 'string'\n * // - age: type 'number'\n * // - active: type 'boolean'\n * // - hireDate: type 'date'\n *\n * // Access inferred result programmatically\n * const config = await grid.getConfig();\n * console.log(config.columns); // Inferred columns\n * ```\n *\n * @see {@link ColumnConfig} for column configuration options\n * @see {@link ColumnType} for type inference rules\n * @since 0.1.1\n */\nexport interface InferredColumnResult<TRow = unknown> {\n /** Generated column configurations based on data analysis */\n columns: ColumnConfigMap<TRow>;\n /** Map of field names to their inferred types */\n typeMap: Record<string, ColumnType>;\n}\n\n/**\n * Column sizing mode.\n *\n * - `'fixed'` - Columns use their configured widths. Horizontal scrolling if content overflows.\n * - `'stretch'` - Columns stretch proportionally to fill available width. No horizontal scrolling.\n *\n * @example\n * ```typescript\n * // Fixed widths - good for many columns\n * grid.fitMode = 'fixed';\n *\n * // Stretch to fill - good for few columns\n * grid.fitMode = 'stretch';\n *\n * // Via gridConfig\n * grid.gridConfig = { fitMode: 'stretch' };\n * ```\n * @since 0.1.1\n */\nexport const FitModeEnum = {\n STRETCH: 'stretch',\n FIXED: 'fixed',\n} as const;\n/**\n * Column sizing mode — determines how columns fill the available grid width.\n * Use `FitModeEnum` to access individual values by key.\n * @since 0.1.1\n */\nexport type FitMode = (typeof FitModeEnum)[keyof typeof FitModeEnum];\n\n/**\n * How automatic column inference combines with explicitly provided columns.\n *\n * - `'auto'` (default): infer columns only when none are provided. Declaring a\n * single column (via `columns`, `gridConfig.columns`, or `<tbw-grid-column>`)\n * disables inference and renders only the declared column(s).\n * - `'merge'`: always infer the full column set from the data (in data-key\n * order), then overlay any explicitly provided columns matched by `field`.\n * A provided column customizes only its own field and keeps its data\n * position; provided columns for fields absent from the data are appended as\n * computed columns.\n *\n * Use `ColumnInferenceModeEnum` to access individual values by key.\n * @since 2.17.0\n */\nexport const ColumnInferenceModeEnum = {\n AUTO: 'auto',\n MERGE: 'merge',\n} as const;\n/**\n * Column inference mode — see {@link ColumnInferenceModeEnum} for values.\n * @since 2.17.0\n */\nexport type ColumnInferenceMode = (typeof ColumnInferenceModeEnum)[keyof typeof ColumnInferenceModeEnum];\n// #endregion\n\n// #region Plugin Interface\n/**\n * Minimal plugin interface for type-checking.\n * This interface is defined here to avoid circular imports with BaseGridPlugin.\n * All plugins must satisfy this shape (BaseGridPlugin implements it).\n *\n * @example\n * ```typescript\n * // Using plugins in grid config\n * import { SelectionPlugin, FilteringPlugin } from '@toolbox-web/grid/all';\n *\n * grid.gridConfig = {\n * plugins: [\n * new SelectionPlugin({ mode: 'row' }),\n * new FilteringPlugin({ debounceMs: 200 }),\n * ],\n * };\n *\n * // Accessing plugin instance at runtime (preferred)\n * const selection = grid.getPluginByName('selection');\n * if (selection) {\n * selection.selectAll();\n * }\n * ```\n *\n * @category Plugin Development\n * @since 0.2.3\n */\nexport interface GridPlugin {\n /** Unique plugin identifier */\n readonly name: string;\n /** Plugin version */\n readonly version: string;\n /** CSS styles to inject into the grid */\n readonly styles?: string;\n}\n\n/**\n * Plugin name-to-type registry for type-safe `getPluginByName()`.\n *\n * Plugins augment this interface via `declare module` so that\n * `grid.getPluginByName('editing')` returns `EditingPlugin | undefined`\n * instead of `GridPlugin | undefined`.\n *\n * @example\n * ```typescript\n * // Plugin augmentation (done automatically when you import a plugin):\n * declare module '../../core/types' {\n * interface PluginNameMap {\n * editing: EditingPlugin;\n * }\n * }\n *\n * // Consumer usage — fully typed:\n * const editing = grid.getPluginByName('editing');\n * editing?.beginBulkEdit(0); // ✅ No cast needed\n * ```\n *\n * @category Plugin Development\n */\n// eslint-disable-next-line @typescript-eslint/no-empty-object-type, @typescript-eslint/no-empty-interface\nexport interface PluginNameMap {}\n// #endregion\n\n// #region Feature Config\n/**\n * Declarative feature configuration interface.\n *\n * This interface is intentionally empty in core — it is populated via **module augmentation**\n * by feature side-effect imports (`@toolbox-web/grid/features/selection`, etc.).\n * Third-party plugins can also augment this interface to add their own features.\n *\n * @example\n * ```ts\n * // Each feature import augments this interface:\n * import '@toolbox-web/grid/features/selection';\n * import '@toolbox-web/grid/features/filtering';\n *\n * grid.gridConfig = {\n * features: {\n * selection: 'range', // ← typed by selection feature module\n * filtering: { debounceMs: 200 }, // ← typed by filtering feature module\n * },\n * };\n * ```\n *\n * @example Third-party augmentation\n * ```ts\n * declare module '@toolbox-web/grid' {\n * interface FeatureConfig {\n * sparkline?: boolean | SparklineConfig;\n * }\n * }\n * ```\n * @since 1.24.0\n */\n// eslint-disable-next-line @typescript-eslint/no-unused-vars -- TRow must keep this exact name so module augmentations across features merge (TS2428).\nexport interface FeatureConfig<TRow = unknown> {\n /**\n * @internal Sentinel property that makes the interface non-empty so TypeScript's\n * excess-property checking rejects unknown feature keys in object literals.\n * Not assignable at runtime (type is `never`).\n */\n __brand?: never;\n}\n// #endregion\n\n// #region Accessibility Config\n\n/**\n * Default announcement messages for screen reader live regions.\n * Each function returns a localized string for a specific state change.\n * Override individual messages via {@link A11yConfig.messages} for i18n.\n *\n * @group Accessibility\n * @since 2.0.0\n */\nexport interface A11yMessages {\n /** Announced when sorting is applied. */\n sortApplied: (column: string, direction: string) => string;\n /** Announced when sorting is cleared. */\n sortCleared: () => string;\n /** Announced when a filter is applied. */\n filterApplied: (column: string) => string;\n /** Announced when a filter is cleared from a column. */\n filterCleared: (column: string) => string;\n /** Announced when all filters are cleared. */\n allFiltersCleared: () => string;\n /** Announced when a group row is expanded. */\n groupExpanded: (name: string, count: number) => string;\n /** Announced when a group row is collapsed. */\n groupCollapsed: (name: string) => string;\n /** Announced when row selection changes. */\n selectionChanged: (count: number) => string;\n /** Announced when a single column is selected. @since 2.8.0 */\n columnSelected: (label: string) => string;\n /** Announced when multiple columns are selected. @since 2.8.0 */\n columnSelectionChanged: (count: number) => string;\n /** Announced when column selection is cleared. @since 2.8.0 */\n columnSelectionCleared: () => string;\n /** Announced when the active selection axis flips between row and column. @since 2.8.0 */\n selectionAxisChanged: (toAxis: 'row' | 'column') => string;\n /** Announced when row editing starts. */\n editingStarted: (rowIndex: number) => string;\n /** Announced when row editing is committed. */\n editingCommitted: (rowIndex: number) => string;\n /** Announced when data is loaded. */\n dataLoaded: (count: number) => string;\n}\n\n/**\n * Accessibility configuration for controlling screen reader announcements.\n *\n * @group Accessibility\n *\n * @example\n * ```ts\n * // Disable all live announcements\n * a11y: { announcements: false }\n *\n * // Override specific messages for French locale\n * a11y: {\n * messages: {\n * sortApplied: (col, dir) => `Trié par ${col}, ${dir}`,\n * sortCleared: () => 'Tri effacé',\n * },\n * }\n * ```\n * @since 2.0.0\n */\nexport interface A11yConfig {\n /**\n * Enable or disable live region announcements.\n * When `false`, the `aria-live` region remains in the DOM but no messages are set.\n * @defaultValue `true`\n */\n announcements?: boolean;\n /**\n * Custom announcement text overrides for internationalization.\n * Partial — only override the messages you need; defaults are used for the rest.\n */\n messages?: Partial<A11yMessages>;\n}\n\n/**\n * Default English announcement messages.\n * Used when no custom messages are provided via {@link A11yConfig.messages}.\n * @since 2.0.0\n */\nexport const DEFAULT_A11Y_MESSAGES: A11yMessages = {\n sortApplied: (column, direction) => `Sorted by ${column}, ${direction}`,\n sortCleared: () => 'Sort cleared',\n filterApplied: (column) => `Filter applied on ${column}`,\n filterCleared: (column) => `Filter cleared from ${column}`,\n allFiltersCleared: () => 'All filters cleared',\n groupExpanded: (name, count) => `Group ${name} expanded, ${count} rows`,\n groupCollapsed: (name) => `Group ${name} collapsed`,\n selectionChanged: (count) => `${count} rows selected`,\n columnSelected: (label) => `Column ${label} selected`,\n columnSelectionChanged: (count) => `${count} columns selected`,\n columnSelectionCleared: () => 'Column selection cleared',\n selectionAxisChanged: (toAxis) =>\n toAxis === 'column'\n ? 'Row selection cleared, column selection active'\n : 'Column selection cleared, row selection active',\n editingStarted: (rowIndex) => `Editing row ${rowIndex + 1}`,\n editingCommitted: (rowIndex) => `Row ${rowIndex + 1} saved`,\n dataLoaded: (count) => `${count} rows loaded`,\n};\n\n// #endregion\n\n// #region Grid Config\n/**\n * Grid configuration object - the **single source of truth** for grid behavior.\n *\n * Users can configure the grid via multiple input methods, all of which converge\n * into an effective `GridConfig` internally:\n *\n * **Configuration Input Methods:**\n * - `gridConfig` property - direct assignment of this object\n * - `columns` property - shorthand for `gridConfig.columns`\n * - `fitMode` property - shorthand for `gridConfig.fitMode`\n * - Light DOM `<tbw-grid-column>` - declarative columns (merged into `columns`)\n * - Light DOM `<tbw-grid-header>` - declarative shell header (merged into `shell.header`)\n *\n * **Precedence (when same property set multiple ways):**\n * Individual props (`fitMode`) > `columns` prop > Light DOM > `gridConfig`\n *\n * @example\n * ```ts\n * // Via gridConfig (recommended for complex setups)\n * grid.gridConfig = {\n * columns: [{ field: 'name' }, { field: 'age' }],\n * fitMode: 'stretch',\n * plugins: [new SelectionPlugin()],\n * shell: { header: { title: 'My Grid' } }\n * };\n *\n * // Via individual props (convenience for simple cases)\n * grid.columns = [{ field: 'name' }, { field: 'age' }];\n * grid.fitMode = 'stretch';\n * ```\n * @since 0.1.1\n */\nexport interface GridConfig<TRow = any> {\n /**\n * Column definitions. Can also be set via `columns` prop or `<tbw-grid-column>` light DOM.\n * @see {@link ColumnConfig} for column options\n * @see {@link ColumnConfigMap}\n */\n columns?: ColumnConfigMap<TRow>;\n /**\n * Dynamic CSS class(es) for data rows.\n * Called for each row during rendering. Return class names to add to the row element.\n *\n * @example\n * ```typescript\n * // Highlight inactive rows\n * rowClass: (row) => row.active ? [] : ['inactive', 'dimmed']\n *\n * // Status-based row styling\n * rowClass: (row) => [`priority-${row.priority}`]\n *\n * // Single class as string\n * rowClass: (row) => row.isNew ? 'new-row' : ''\n * ```\n */\n rowClass?: (row: TRow) => string | string[];\n /** Sizing mode for columns. Can also be set via `fitMode` prop. */\n fitMode?: FitMode;\n\n /**\n * How automatic column inference combines with explicitly provided columns.\n * Can also be set via the `columnInference` prop or `column-inference` attribute.\n *\n * - `'auto'` (default): infer only when no columns are provided (current behavior).\n * - `'merge'`: always infer from data, then overlay provided columns by `field`.\n *\n * @see {@link ColumnInferenceMode}\n * @since 2.17.0\n */\n columnInference?: ColumnInferenceMode;\n /**\n * Grid-wide sorting toggle.\n * When false, disables sorting for all columns regardless of their individual `sortable` setting.\n * When true (default), columns with `sortable: true` can be sorted.\n *\n * This affects:\n * - Header click handlers for sorting\n * - Sort indicator visibility\n * - Multi-sort plugin behavior (if loaded)\n *\n * @default true\n *\n * @example\n * ```typescript\n * // Disable all sorting\n * gridConfig = { sortable: false };\n *\n * // Enable sorting (default) - individual columns still need sortable: true\n * gridConfig = { sortable: true };\n * ```\n */\n sortable?: boolean;\n\n /**\n * Grid-wide resizing toggle.\n * When false, disables column resizing for all columns regardless of their individual `resizable` setting.\n * When true (default), columns with `resizable: true` (or resizable not set, since it defaults to true) can be resized.\n *\n * This affects:\n * - Resize handle visibility in header cells\n * - Double-click to auto-size behavior\n *\n * @default true\n *\n * @example\n * ```typescript\n * // Disable all column resizing\n * gridConfig = { resizable: false };\n *\n * // Enable resizing (default) - individual columns can opt out with resizable: false\n * gridConfig = { resizable: true };\n * ```\n */\n resizable?: boolean;\n\n /**\n * Row height in pixels for virtualization calculations.\n * The virtualization system assumes uniform row heights for performance.\n *\n * If not specified, the grid measures the first rendered row's height,\n * which respects the CSS variable `--tbw-row-height` set by themes.\n *\n * Set this explicitly when:\n * - Row content may wrap to multiple lines (also set `--tbw-cell-white-space: normal`)\n * - Using custom row templates with variable content\n * - You want to override theme-defined row height\n * - Rows have different heights based on content (use function form)\n *\n * **Variable Row Heights**: When a function is provided, the grid enables variable height\n * virtualization. Heights are measured on first render and cached by row identity.\n *\n * @default Auto-measured from first row (respects --tbw-row-height CSS variable)\n *\n * @example\n * ```ts\n * // Fixed height for all rows\n * gridConfig = { rowHeight: 56 };\n *\n * // Variable height based on content\n * gridConfig = {\n * rowHeight: (row, index) => row.hasDetails ? 80 : 40,\n * };\n *\n * // Return undefined to trigger DOM auto-measurement\n * gridConfig = {\n * rowHeight: (row) => row.isExpanded ? undefined : 40,\n * };\n * ```\n */\n rowHeight?: number | ((row: TRow, index: number) => number | undefined);\n /**\n * Array of plugin instances.\n * Each plugin is instantiated with its configuration and attached to this grid.\n *\n * @example\n * ```ts\n * plugins: [\n * new SelectionPlugin({ mode: 'range' }),\n * new MultiSortPlugin(),\n * new FilteringPlugin({ debounceMs: 150 }),\n * ]\n * ```\n */\n plugins?: GridPlugin[];\n\n /**\n * Declarative feature configuration.\n * Alternative to manually creating plugin instances in `plugins`.\n * Features are resolved using the core feature registry.\n *\n * Import feature modules as side effects to register them:\n * ```ts\n * import '@toolbox-web/grid/features/selection';\n * import '@toolbox-web/grid/features/filtering';\n * ```\n *\n * Then configure declaratively:\n * ```ts\n * gridConfig = {\n * features: {\n * selection: 'range',\n * filtering: { debounceMs: 200 },\n * editing: 'dblclick',\n * },\n * };\n * ```\n *\n * Both `features` and `plugins` can be used together — features-generated plugins\n * are created first, then manual `plugins` are appended. Duplicates are skipped\n * (manual `plugins` take precedence).\n */\n features?: Partial<FeatureConfig<TRow>>;\n\n /**\n * Saved column state to restore on initialization.\n * Includes order, width, visibility, sort, and plugin-contributed state.\n */\n columnState?: GridColumnState;\n\n // NOTE: `shell?: ShellConfig` is contributed via module augmentation from\n // `plugins/shell/types.ts` (extraction #370). Core has no intrinsic shell field.\n\n /**\n * Grid-wide icon configuration.\n *\n * The grid uses a **CSS-first hybrid icon system**:\n * - **Default (CSS):** Icons render via `--tbw-icon-*` CSS custom properties on `tbw-grid`.\n * Override them in your theme CSS — no JavaScript needed.\n * - **JS override:** Setting `gridConfig.icons` takes precedence over CSS for any key provided.\n * Use this for dynamic icons, icon libraries, or `HTMLElement` instances.\n *\n * All icons are optional — sensible defaults are used when not specified.\n * Plugins will use these by default but can override with their own config.\n */\n icons?: GridIcons;\n\n /**\n * Grid-wide animation configuration.\n * Controls animations for expand/collapse, reordering, and other visual transitions.\n * Individual plugins can override these defaults in their own config.\n */\n animation?: AnimationConfig;\n\n /**\n * Custom sort handler for the entire grid.\n *\n * :::caution\n * **Prefer {@link BaseColumnConfig.sortComparator} over `sortHandler`.**\n *\n * `sortHandler` is a low-level escape hatch with significant limitations:\n * - Only consulted by the **single-column** sort path (core header click,\n * `TreePlugin` per-level sort, `ServerSidePlugin` `sortMode: 'local'`).\n * - **Bypassed entirely** when `MultiSortPlugin` is loaded.\n * - Owns ALL columns at once — your handler must implement field/direction\n * dispatch and null handling for every sortable column itself.\n *\n * For per-column custom sort logic, use {@link BaseColumnConfig.sortComparator}\n * instead. It is honored by every sort code path in the grid (core, multi-sort,\n * tree, server-side) and is composable across columns.\n *\n * For server-side sort, prefer `ServerSideConfig.dataSource` — the\n * `sortModel` is shipped to your `getRows` handler so the backend can return\n * pre-sorted blocks.\n * :::\n *\n * Use `sortHandler` only when you need to replace the grid's sort engine\n * wholesale (e.g. integrating a third-party sort library that operates on\n * the full row array, or routing every sort through a single async pipeline).\n *\n * The handler receives:\n * - `rows`: Current row array to sort\n * - `sortState`: Sort field and direction (1 = asc, -1 = desc)\n * - `columns`: Column configurations (for accessing sortComparator)\n *\n * Return the sorted array (sync) or a Promise that resolves to it (async).\n *\n * @example\n * ```ts\n * // Replace the entire client-side sort engine with a custom stable sort\n * sortHandler: (rows, state) => stableSort(rows, state.field, state.direction);\n * ```\n *\n * @see {@link BaseColumnConfig.sortComparator} — recommended per-column override\n * @see `ServerSideConfig.dataSource` — recommended server-side sort path\n */\n sortHandler?: SortHandler<TRow>;\n\n /**\n * Initial sort state applied when the grid first renders.\n *\n * Equivalent to calling `grid.sort(field, direction)` after the grid is created,\n * but avoids the imperative call and extra render cycle.\n *\n * @example\n * ```ts\n * gridConfig = {\n * initialSort: { field: 'salary', direction: 'desc' },\n * };\n * ```\n *\n * @see {@link DataGridElement.sort} for runtime sorting\n * @see {@link DataGridElement.sortModel} for reading current sort state\n */\n initialSort?: { field: string; direction: 'asc' | 'desc' };\n\n /**\n * Function to extract a unique identifier from a row.\n * Used by `updateRow()`, `getRow()`, and ID-based tracking.\n *\n * If not provided, falls back to `row.id` or `row._id` if present.\n * Rows without IDs are silently skipped during map building.\n * Only throws when explicitly calling `getRowId()` or `updateRow()` on a row without an ID.\n *\n * @example\n * ```ts\n * // Simple field\n * getRowId: (row) => row.id\n *\n * // Composite key\n * getRowId: (row) => `${row.voyageId}-${row.legNumber}`\n *\n * // UUID field\n * getRowId: (row) => row.uuid\n * ```\n */\n getRowId?: (row: TRow) => string;\n\n /**\n * Type-level renderer and editor defaults.\n *\n * Keys can be:\n * - Built-in types: `'string'`, `'number'`, `'date'`, `'boolean'`, `'select'`\n * - Custom types: `'currency'`, `'country'`, `'status'`, etc.\n *\n * Resolution order (highest priority first):\n * 1. Column-level (`column.renderer` / `column.editor`)\n * 2. Grid-level (`gridConfig.typeDefaults[column.type]`)\n * 3. App-level (Angular `GridTypeRegistry`, React `GridTypeProvider`)\n * 4. Built-in (checkbox for boolean, select for select, etc.)\n * 5. Fallback (plain text / text input)\n *\n * @example\n * ```typescript\n * typeDefaults: {\n * date: { editor: myDatePickerEditor },\n * country: {\n * renderer: (ctx) => {\n * const span = document.createElement('span');\n * span.innerHTML = `<img src=\"/flags/${ctx.value}.svg\" /> ${ctx.value}`;\n * return span;\n * },\n * editor: (ctx) => createCountrySelect(ctx)\n * }\n * }\n * ```\n */\n typeDefaults?: Record<string, TypeDefault<TRow>>;\n\n // #region Accessibility\n\n /**\n * Accessible label for the grid.\n * Sets `aria-label` on the grid's internal table element for screen readers.\n *\n * If not provided and `shell.header.title` is set, the title is used automatically.\n *\n * If [`gridAriaLabelledBy`](#gridarialabelledby) is also set, `aria-labelledby`\n * takes precedence per WAI-ARIA accessible-name computation and `aria-label`\n * is omitted.\n *\n * @example\n * ```ts\n * gridConfig = { gridAriaLabel: 'Employee data' };\n * ```\n */\n gridAriaLabel?: string;\n\n /**\n * ID of an element that labels the grid.\n * Sets `aria-labelledby` on the grid's internal table element so screen\n * readers can use the referenced element's text as the accessible name —\n * useful when the grid already sits next to a heading.\n *\n * Per WAI-ARIA accessible-name precedence, `aria-labelledby` takes priority\n * over `aria-label` and over the auto-derived shell title. When this option\n * is set, the grid omits `aria-label` to avoid conflicting names.\n *\n * @example\n * ```html\n * <h2 id=\"grid-heading\">Employees</h2>\n * <tbw-grid></tbw-grid>\n * ```\n * ```ts\n * gridConfig = { gridAriaLabelledBy: 'grid-heading' };\n * ```\n */\n gridAriaLabelledBy?: string;\n\n /**\n * ID of an element that describes the grid.\n * Sets `aria-describedby` on the grid's internal table element.\n *\n * @example\n * ```html\n * <p id=\"grid-desc\">This table shows all active employees.</p>\n * <tbw-grid></tbw-grid>\n * ```\n * ```ts\n * gridConfig = { gridAriaDescribedBy: 'grid-desc' };\n * ```\n */\n gridAriaDescribedBy?: string;\n\n /**\n * Override the screen-reader-announced role name for the grid via\n * `aria-roledescription`. Useful for localization (e.g. `\"Tabell\"` in\n * Norwegian) or domain-specific naming (e.g. `\"Employee table\"`).\n *\n * :::caution\n * Per [WAI-ARIA 1.2](https://www.w3.org/TR/wai-aria-1.2/#aria-roledescription),\n * the value should still describe a grid-like widget. Overriding with an\n * unrelated label confuses assistive-technology users about the available\n * interactions (cell navigation, sort, etc.). Leave unset to use the\n * default role name announced by the AT.\n * :::\n *\n * @example\n * ```ts\n * gridConfig = { gridAriaRoleDescription: 'Employee table' };\n * ```\n */\n gridAriaRoleDescription?: string;\n\n /**\n * Accessibility configuration for screen reader announcements.\n *\n * The grid automatically announces state changes (sort, filter, selection, etc.)\n * via an `aria-live` region. Use this config to toggle announcements or override\n * message text for internationalization.\n *\n * @example\n * ```ts\n * // Disable all announcements\n * gridConfig = { a11y: { announcements: false } };\n *\n * // Custom messages for i18n\n * gridConfig = {\n * a11y: {\n * messages: {\n * sortApplied: (col, dir) => `Trié par ${col}, ${dir}`,\n * filterApplied: (col) => `Filtre appliqué sur ${col}`,\n * },\n * },\n * };\n * ```\n */\n a11y?: A11yConfig;\n\n // #endregion\n\n // #region Loading\n\n /**\n * Custom renderer for the loading overlay.\n *\n * When provided, replaces the default spinner with custom content.\n * Receives a context object with the current loading size.\n *\n * @example\n * ```typescript\n * // Simple text loading indicator\n * loadingRenderer: () => {\n * const el = document.createElement('div');\n * el.textContent = 'Loading...';\n * return el;\n * }\n *\n * // Custom spinner component\n * loadingRenderer: (ctx) => {\n * const spinner = document.createElement('my-spinner');\n * spinner.size = ctx.size === 'large' ? 48 : 24;\n * return spinner;\n * }\n * ```\n */\n loadingRenderer?: LoadingRenderer;\n\n // #endregion\n\n // #region Empty State\n\n /**\n * Custom renderer shown when the grid has no rows to display\n * (`loading === false` AND the rendered row count is `0`, after all plugin\n * processing such as filtering / grouping / server-side).\n *\n * - When **omitted**, a built-in message is rendered (\"No data to display\"\n * or \"No matching rows\" when source rows existed but were filtered out).\n * - When set to a function, the function receives an {@link EmptyContext}\n * and returns an `HTMLElement` or HTML string.\n * - When **explicitly `null`**, the empty overlay is suppressed entirely.\n *\n * The empty overlay is mutually exclusive with the loading overlay; if\n * `loading === true`, the loading overlay always wins.\n *\n * @example\n * ```typescript\n * // Show a backend error message via a closure over the consumer's state.\n * gridConfig.emptyRenderer = () =>\n * error\n * ? `Failed to load deals: ${error.message}`\n * : 'No deals to display';\n * ```\n *\n * @see {@link EmptyOverlay} to control where the overlay is mounted.\n * @since 2.12.0\n */\n emptyRenderer?: EmptyRenderer | null;\n\n /**\n * Where the empty-state overlay is mounted.\n *\n * - `'rows'` (default) — overlays the `.rows-container`. Headers stay\n * visible so users can clear filters or see the column schema.\n * - `'grid'` — overlays the `.tbw-grid-root`. Hides headers and any\n * shell/toolbar content too.\n *\n * @defaultValue `'rows'`\n * @since 2.12.0\n */\n emptyOverlay?: EmptyOverlay;\n\n // #endregion\n}\n// #endregion\n\n// #region Empty State Types\n\n/**\n * Where the empty-state overlay is mounted.\n *\n * @see {@link GridConfig.emptyOverlay}\n * @since 2.12.0\n */\nexport type EmptyOverlay = 'rows' | 'grid';\n\n/**\n * Context passed to a custom {@link EmptyRenderer}.\n *\n * @since 2.12.0\n */\nexport interface EmptyContext {\n /**\n * Number of rows in the source data before any plugin processing\n * (sort / filter / group / server-side). Equivalent to\n * `grid.sourceRows.length`.\n */\n sourceRowCount: number;\n /**\n * `true` when the source had rows but all of them were filtered out\n * (i.e. `sourceRowCount > 0` while the rendered row count is `0`).\n * The default renderer uses this flag to switch between\n * \"No data to display\" and \"No matching rows\".\n */\n filteredOut: boolean;\n}\n\n/**\n * Custom renderer for the empty state overlay.\n *\n * @param context - {@link EmptyContext} describing why the grid is empty.\n * @returns An `HTMLElement` or an HTML string.\n *\n * @example\n * ```typescript\n * const renderer: EmptyRenderer = (ctx) => {\n * const div = document.createElement('div');\n * div.textContent = ctx.filteredOut ? 'No matches' : 'No data';\n * return div;\n * };\n * ```\n *\n * @see {@link GridConfig.emptyRenderer}\n * @since 2.12.0\n */\nexport type EmptyRenderer = (context: EmptyContext) => HTMLElement | string;\n\n// #endregion\n\n// #region Animation\n\n/**\n * Sort state passed to custom sort handlers.\n * Represents the current sorting configuration for a column.\n *\n * @example\n * ```typescript\n * // In a custom sort handler\n * const sortHandler: SortHandler = (rows, sortState, columns) => {\n * const { field, direction } = sortState;\n * console.log(`Sorting by ${field} ${direction === 1 ? 'ASC' : 'DESC'}`);\n *\n * return [...rows].sort((a, b) => {\n * const aVal = a[field];\n * const bVal = b[field];\n * return (aVal < bVal ? -1 : aVal > bVal ? 1 : 0) * direction;\n * });\n * };\n * ```\n *\n * @see {@link SortHandler} for custom sort handler signature\n * @see {@link SortChangeDetail} for sort change events\n * @since 0.2.7\n */\nexport interface SortState {\n /** Field to sort by */\n field: string;\n /** Sort direction: 1 = ascending, -1 = descending */\n direction: 1 | -1;\n}\n\n/**\n * Custom sort handler function signature.\n *\n * :::caution\n * **Prefer {@link BaseColumnConfig.sortComparator} over `SortHandler`.**\n * `sortHandler` is bypassed by `MultiSortPlugin` and only sees the single\n * active sort field. For per-column custom sort logic that survives every\n * sort code path (core, multi-sort, tree, server-side), set `sortComparator`\n * on the relevant columns instead. For server-side sort, use\n * `ServerSideConfig.dataSource`.\n * :::\n *\n * Use `SortHandler` only when you need to replace the grid's sort engine\n * wholesale.\n *\n * @param rows - Current row array to sort\n * @param sortState - Sort field and direction\n * @param columns - Column configurations (for accessing sortComparator)\n * @returns Sorted array (sync) or Promise resolving to sorted array (async)\n *\n * @example\n * ```typescript\n * // Replace the built-in sort with a third-party stable sort library\n * const customSortHandler: SortHandler<Employee> = (rows, state) => {\n * return thirdPartyStableSort(rows, state.field, state.direction);\n * };\n *\n * grid.gridConfig = { sortHandler: customSortHandler };\n * ```\n *\n * @see {@link SortState} for the sort state object\n * @see {@link GridConfig.sortHandler} for configuring the handler (and its caveats)\n * @see {@link BaseColumnConfig.sortComparator} — recommended per-column comparator\n * @since 0.2.7\n */\nexport type SortHandler<TRow = any> = (\n rows: TRow[],\n sortState: SortState,\n columns: ColumnConfig<TRow>[],\n) => TRow[] | Promise<TRow[]>;\n\n// #region Loading\n\n/**\n * Loading indicator size variant.\n *\n * - `'large'`: 48x48px max - used for grid-level loading overlay (`grid.loading = true`)\n * - `'small'`: Follows row height - used for row/cell loading states\n *\n * @example\n * ```typescript\n * // Custom loading renderer adapting to size\n * const myLoader: LoadingRenderer = (ctx) => {\n * if (ctx.size === 'large') {\n * // Full overlay spinner\n * return '<div class=\"spinner-lg\"></div>';\n * }\n * // Inline row/cell spinner\n * return '<span class=\"spinner-sm\"></span>';\n * };\n * ```\n *\n * @see {@link LoadingRenderer} for custom loading renderer\n * @see {@link LoadingContext} for context passed to renderers\n * @since 1.7.0\n */\nexport type LoadingSize = 'large' | 'small';\n\n/**\n * Context passed to custom loading renderers.\n *\n * Provides information about the loading indicator being rendered,\n * allowing the renderer to adapt its appearance based on the size variant.\n *\n * @example\n * ```typescript\n * const myLoadingRenderer: LoadingRenderer = (ctx: LoadingContext) => {\n * if (ctx.size === 'large') {\n * // Full-size spinner for grid-level loading\n * return '<div class=\"large-spinner\"></div>';\n * } else {\n * // Compact spinner for row/cell loading\n * return '<div class=\"small-spinner\"></div>';\n * }\n * };\n * ```\n *\n * @see {@link LoadingRenderer} for the renderer function signature\n * @see {@link LoadingSize} for available size variants\n * @since 1.7.0\n */\nexport interface LoadingContext {\n /** The size variant being rendered: 'large' for grid-level, 'small' for row/cell */\n size: LoadingSize;\n}\n\n/**\n * Custom loading renderer function.\n * Returns an element or HTML string to display as the loading indicator.\n *\n * Used with the `loadingRenderer` property in {@link GridConfig} to replace\n * the default spinner with custom content.\n *\n * @param context - Context containing size information\n * @returns HTMLElement or HTML string\n *\n * @example\n * ```typescript\n * // Simple text loading indicator\n * const textLoader: LoadingRenderer = () => {\n * const el = document.createElement('div');\n * el.textContent = 'Loading...';\n * return el;\n * };\n *\n * // Custom spinner with size awareness\n * const customSpinner: LoadingRenderer = (ctx) => {\n * const spinner = document.createElement('my-spinner');\n * spinner.size = ctx.size === 'large' ? 48 : 24;\n * return spinner;\n * };\n *\n * // Material Design-style progress bar\n * const progressBar: LoadingRenderer = () => {\n * const container = document.createElement('div');\n * container.className = 'progress-bar-container';\n * container.innerHTML = '<div class=\"progress-bar\"></div>';\n * return container;\n * };\n *\n * grid.gridConfig = {\n * loadingRenderer: customSpinner,\n * };\n * ```\n *\n * @see {@link LoadingContext} for the context object passed to the renderer\n * @see {@link LoadingSize} for size variants ('large' | 'small')\n * @since 1.7.0\n */\nexport type LoadingRenderer = (context: LoadingContext) => HTMLElement | string;\n\n// #endregion\n\n// #region Data Change Event\n\n/**\n * Detail for the `data-change` event.\n *\n * Fired whenever the grid's row data changes — including new data assignment,\n * row insertion/removal, and in-place mutations via `updateRow()`.\n *\n * Use this to keep external UI in sync with the grid's current data state\n * (row counts, summaries, charts, etc.).\n *\n * @example\n * ```typescript\n * grid.on('data-change', ({ rowCount, sourceRowCount }) => {\n * console.log(`${rowCount} rows visible of ${sourceRowCount} total`);\n * });\n * ```\n *\n * @see {@link DataGridEventMap} for all event types\n * @category Events\n * @since 1.25.0\n */\nexport interface DataChangeDetail {\n /** Number of visible (processed) rows */\n rowCount: number;\n /** Total number of source rows (before filtering/grouping) */\n sourceRowCount: number;\n}\n\n/**\n * Detail payload for the `tbw-scroll` event.\n *\n * Dispatched on the grid host whenever the vertical viewport scrolls\n * (rAF-batched — at most one per frame). Use to trigger pagination,\n * defer heavy cell content rendering, dismiss overlays, or sync a\n * scroll-tracking UI outside the grid.\n *\n * For server-side pagination of large datasets, prefer\n * `ServerSidePlugin` which handles block fetching out of the box.\n *\n * @example\n * ```typescript\n * grid.addEventListener('tbw-scroll', (e) => {\n * const { scrollTop, scrollHeight, clientHeight } = e.detail;\n * if (scrollTop + clientHeight >= scrollHeight - 200) loadMore();\n * });\n * ```\n *\n * @see {@link DataGridEventMap} for all event types\n * @category Events\n * @since 2.2.0\n */\nexport interface TbwScrollDetail {\n /** Current vertical scroll offset in pixels (faux scrollbar). */\n scrollTop: number;\n /** Total scrollable height in pixels (faux scrollbar). */\n scrollHeight: number;\n /** Visible viewport height in pixels (faux scrollbar). */\n clientHeight: number;\n /**\n * Axis that triggered this dispatch. Currently always `'vertical'`;\n * `'horizontal'` is reserved for a future opt-in dispatch and is\n * declared up-front so consumer narrowing remains source-compatible.\n */\n direction: 'vertical' | 'horizontal';\n}\n\n// #endregion\n\n// #region Data Update Management\n\n/**\n * Indicates the origin of a data change.\n * Used to prevent infinite loops in cascade update handlers.\n *\n * - `'user'`: Direct user interaction via EditingPlugin (typing, selecting)\n * - `'cascade'`: Triggered by `updateRow()` in an event handler\n * - `'api'`: External programmatic update via `grid.updateRow()`\n *\n * @example\n * ```typescript\n * grid.on('cell-change', (detail) => {\n * const { source, field, newValue } = detail;\n *\n * // Only cascade updates for user edits\n * if (source === 'user' && field === 'price') {\n * // Update calculated field (marked as 'cascade')\n * grid.updateRow(detail.rowId, {\n * total: newValue * detail.row.quantity,\n * });\n * }\n *\n * // Ignore cascade updates to prevent infinite loops\n * if (source === 'cascade') return;\n * });\n * ```\n *\n * @see {@link CellChangeDetail} for the event detail containing source\n * @category Data Management\n * @since 1.0.0\n */\nexport type UpdateSource = 'user' | 'cascade' | 'api';\n\n/**\n * Detail for cell-change event (emitted by core after mutation).\n * This is an informational event that fires for ALL data mutations.\n *\n * Use this event for:\n * - Logging/auditing changes\n * - Cascading updates (updating other fields based on a change)\n * - Syncing changes to external state\n *\n * @example\n * ```typescript\n * grid.on('cell-change', ({ row, rowId, field, oldValue, newValue, source }) => {\n * console.log(`${field} changed from ${oldValue} to ${newValue}`);\n * console.log(`Change source: ${source}`);\n *\n * // Cascade: update total when price changes\n * if (source === 'user' && field === 'price') {\n * grid.updateRow(rowId, { total: newValue * row.quantity });\n * }\n * });\n * ```\n *\n * @see {@link UpdateSource} for understanding change origins\n * @see CellCommitDetail for the commit event (editing lifecycle)\n * @category Events\n * @since 1.0.0\n */\nexport interface CellChangeDetail<TRow = unknown> {\n /** The row object (after mutation) */\n row: TRow;\n /** Stable row identifier */\n rowId: string;\n /** Current index in rows array */\n rowIndex: number;\n /** Field that changed */\n field: string;\n /** Value before change */\n oldValue: unknown;\n /** Value after change */\n newValue: unknown;\n /** All changes passed to updateRow/updateRows (for context) */\n changes: Partial<TRow>;\n /** Origin of this change */\n source: UpdateSource;\n}\n\n/**\n * Batch update specification for updateRows().\n *\n * Used when you need to update multiple rows at once efficiently.\n * The grid will batch all updates and trigger a single re-render.\n *\n * @example\n * ```typescript\n * // Update multiple rows in a single batch\n * const updates: RowUpdate<Employee>[] = [\n * { id: 'emp-1', changes: { status: 'active', updatedAt: new Date() } },\n * { id: 'emp-2', changes: { status: 'inactive' } },\n * { id: 'emp-3', changes: { salary: 75000 } },\n * ];\n *\n * grid.updateRows(updates);\n * ```\n *\n * @see {@link CellChangeDetail} for individual change events\n * @see {@link GridConfig.getRowId} for row identification\n * @category Data Management\n * @since 1.0.0\n */\nexport interface RowUpdate<TRow = unknown> {\n /** Row identifier (from getRowId) */\n id: string;\n /** Fields to update */\n changes: Partial<TRow>;\n}\n\n/**\n * A batch of row mutations to apply atomically in a single render cycle.\n *\n * All adds, updates, and removes are processed together with one re-render,\n * making this far more efficient than calling `insertRow`, `updateRow`, and\n * `removeRow` individually — especially for high-frequency streaming data.\n *\n * Row identification for `update` and `remove` uses the grid's configured\n * {@link GridConfig.getRowId | getRowId} function.\n *\n * @example\n * ```typescript\n * // Apply a mixed transaction from a WebSocket message\n * const result = await grid.applyTransaction({\n * add: [{ id: 'new-1', name: 'Alice', status: 'Active' }],\n * update: [{ id: 'emp-5', changes: { status: 'Inactive' } }],\n * remove: [{ id: 'emp-3' }],\n * });\n *\n * console.log(`Added: ${result.added.length}, Updated: ${result.updated.length}, Removed: ${result.removed.length}`);\n * ```\n *\n * @see {@link TransactionResult} for the result structure\n * @category Data Management\n * @since 1.27.0\n */\nexport interface RowTransaction<TRow = unknown> {\n /** Rows to insert. Appended at the end of the current view. */\n add?: TRow[];\n /** Rows to update in-place by ID. */\n update?: RowUpdate<TRow>[];\n /** Rows to remove by ID. */\n remove?: Array<{ id: string }>;\n}\n\n/**\n * Result of a {@link RowTransaction} applied via `applyTransaction`.\n *\n * Contains the actual row objects that were affected, useful for\n * post-processing or logging.\n *\n * @see {@link RowTransaction} for the input structure\n * @category Data Management\n * @since 1.27.0\n */\nexport interface TransactionResult<TRow = unknown> {\n /** Rows that were successfully added. */\n added: TRow[];\n /** Rows that were successfully updated (references to the mutated row objects). */\n updated: TRow[];\n /** Rows that were successfully removed. */\n removed: TRow[];\n}\n\n// #endregion\n\n/**\n * Animation behavior mode.\n * - `true` or `'on'`: Animations always enabled\n * - `false` or `'off'`: Animations always disabled\n * - `'reduced-motion'`: Respects `prefers-reduced-motion` media query (default)\n *\n * @example\n * ```typescript\n * // Force animations on (ignore system preference)\n * grid.gridConfig = { animation: { mode: 'on' } };\n *\n * // Disable all animations\n * grid.gridConfig = { animation: { mode: false } };\n *\n * // Respect user's accessibility settings (default)\n * grid.gridConfig = { animation: { mode: 'reduced-motion' } };\n * ```\n *\n * @see {@link AnimationConfig} for full animation configuration\n * @since 0.2.7\n */\nexport type AnimationMode = boolean | 'on' | 'off' | 'reduced-motion';\n\n/**\n * Animation style for visual transitions.\n * - `'slide'`: Slide/transform animation (e.g., expand down, slide left/right)\n * - `'fade'`: Opacity fade animation\n * - `'flip'`: FLIP technique for position changes (First, Last, Invert, Play)\n * - `false`: No animation for this specific feature\n *\n * @example\n * ```typescript\n * // Plugin-specific animation styles\n * new TreePlugin({\n * expandAnimation: 'slide', // Slide children down when expanding\n * });\n *\n * new ReorderPlugin({\n * animation: 'flip', // FLIP animation for column reordering\n * });\n * ```\n *\n * @see {@link AnimationConfig} for grid-wide animation settings\n * @see {@link ExpandCollapseAnimation} for expand/collapse-specific styles\n * @since 0.2.7\n */\nexport type AnimationStyle = 'slide' | 'fade' | 'flip' | false;\n\n/**\n * Animation style for expand/collapse operations.\n * Subset of AnimationStyle - excludes 'flip' which is for position changes.\n * - `'slide'`: Slide down/up animation for expanding/collapsing content\n * - `'fade'`: Fade in/out animation\n * - `false`: No animation\n *\n * @example\n * ```typescript\n * // Tree rows slide down when expanding\n * new TreePlugin({ expandAnimation: 'slide' });\n *\n * // Row groups fade in/out\n * new GroupingRowsPlugin({ expandAnimation: 'fade' });\n *\n * // Master-detail panels with no animation\n * new MasterDetailPlugin({ expandAnimation: false });\n * ```\n *\n * @see {@link AnimationStyle} for all animation styles\n * @see {@link AnimationConfig} for grid-wide settings\n * @since 0.2.9\n */\nexport type ExpandCollapseAnimation = 'slide' | 'fade' | false;\n\n/**\n * Type of row animation.\n * - `'change'`: Flash highlight when row data changes (e.g., after cell edit)\n * - `'insert'`: Slide-in animation for newly added rows\n * - `'remove'`: Fade-out animation for rows being removed\n *\n * @example\n * ```typescript\n * // Internal usage - row animation is triggered automatically:\n * // - 'change' after cell-commit event\n * // - 'insert' when rows are added to the grid\n * // - 'remove' when rows are deleted\n *\n * // The animation respects AnimationConfig.mode\n * grid.gridConfig = {\n * animation: { mode: 'on', duration: 300 },\n * };\n * ```\n *\n * @see {@link AnimationConfig} for animation configuration\n * @since 1.3.0\n */\nexport type RowAnimationType = 'change' | 'insert' | 'remove';\n\n/**\n * Grid-wide animation configuration.\n * Controls global animation behavior - individual plugins define their own animation styles.\n * Duration and easing values set corresponding CSS variables on the grid element.\n *\n * @example\n * ```typescript\n * // Enable animations regardless of system preferences\n * grid.gridConfig = {\n * animation: {\n * mode: 'on',\n * duration: 300,\n * easing: 'cubic-bezier(0.4, 0, 0.2, 1)',\n * },\n * };\n *\n * // Disable all animations\n * grid.gridConfig = {\n * animation: { mode: 'off' },\n * };\n *\n * // Respect user's reduced-motion preference (default)\n * grid.gridConfig = {\n * animation: { mode: 'reduced-motion' },\n * };\n * ```\n *\n * @see {@link AnimationMode} for mode options\n * @since 0.2.7\n */\nexport interface AnimationConfig {\n /**\n * Global animation mode.\n * @default 'reduced-motion'\n */\n mode?: AnimationMode;\n\n /**\n * Default animation duration in milliseconds.\n * Sets `--tbw-animation-duration` CSS variable.\n * @default 200\n */\n duration?: number;\n\n /**\n * Default easing function.\n * Sets `--tbw-animation-easing` CSS variable.\n * @default 'ease-out'\n */\n easing?: string;\n}\n\n/** Default animation configuration * @since 0.2.7\n */\nexport const DEFAULT_ANIMATION_CONFIG: Required<Omit<AnimationConfig, 'sort'>> = {\n mode: 'reduced-motion',\n duration: 200,\n easing: 'ease-out',\n};\n\n// #endregion\n\n// #region Grid Icons\n\n/** Icon value - can be a string (text/HTML) or HTMLElement * @since 0.1.1\n */\nexport type IconValue = string | HTMLElement;\n\n/**\n * Grid-wide icon configuration.\n * All icons are optional - sensible defaults are used when not specified.\n *\n * The grid uses a **CSS-first hybrid approach**: icons render via `--tbw-icon-*` CSS\n * custom properties by default. Setting `gridConfig.icons` provides JS overrides that\n * take precedence over CSS (the JS icon injects DOM content, suppressing the CSS\n * `::before` pseudo-element via the `:empty` selector).\n *\n * **Use CSS** for static theming (text, emoji, SVG masks via `--tbw-icon-*-mask`).\n * **Use JS** for dynamic icons, icon libraries, or `HTMLElement` instances.\n *\n * Icons can be text (including emoji), HTML strings (for SVG), or HTMLElement instances.\n *\n * @example\n * ```typescript\n * grid.gridConfig = {\n * icons: {\n * // Emoji icons\n * expand: '➕',\n * collapse: '➖',\n *\n * // Custom SVG icon\n * sortAsc: '<svg viewBox=\"0 0 16 16\"><path d=\"M8 4l4 8H4z\"/></svg>',\n *\n * // Font icon class (wrap in span)\n * filter: '<span class=\"icon icon-filter\"></span>',\n * },\n * };\n * ```\n *\n * @see {@link IconValue} for allowed icon formats\n * @since 0.1.1\n */\nexport interface GridIcons {\n /** Expand icon for collapsed items (trees, groups, details). Default: '▶' */\n expand?: IconValue;\n /** Collapse icon for expanded items (trees, groups, details). Default: '▼' */\n collapse?: IconValue;\n /** Sort ascending indicator. Default: '▲' */\n sortAsc?: IconValue;\n /** Sort descending indicator. Default: '▼' */\n sortDesc?: IconValue;\n /** Sort neutral/unsorted indicator. Default: '⇅' */\n sortNone?: IconValue;\n /** Submenu arrow for context menus. Default: '▶' */\n submenuArrow?: IconValue;\n /** Drag handle icon for reordering. Default: '⋮⋮' */\n dragHandle?: IconValue;\n /** Tool panel toggle icon in toolbar. Default: '☰' */\n toolPanel?: IconValue;\n /** Filter icon in column headers. Default: SVG funnel icon */\n filter?: IconValue;\n /** Filter icon when filter is active. Default: same as filter with accent color */\n filterActive?: IconValue;\n /** Print icon for print button. Default: '🖨️' */\n print?: IconValue;\n}\n\n/** Default icons used when not overridden. Most entries are short text/emoji;\n * `filter` and `filterActive` are empty strings because the actual rendering\n * is driven by the `--tbw-icon-filter[-active]-mask` CSS custom properties\n * (see `core/styles/variables.css`). Userland that wants an HTML/SVG fallback\n * via `gridConfig.icons.filter = '<svg…>'` is unaffected — that path overrides this default. * @since 0.1.1\n */\nexport const DEFAULT_GRID_ICONS: Required<GridIcons> = {\n expand: '▶',\n collapse: '▼',\n sortAsc: '▲',\n sortDesc: '▼',\n sortNone: '⇅',\n submenuArrow: '▶',\n dragHandle: '⋮⋮',\n toolPanel: '☰',\n filter: '',\n filterActive: '',\n print: '🖨️',\n};\n// #endregion\n\n// #region Column State (Persistence)\n\n/**\n * State for a single column. Captures user-driven changes at runtime.\n * Plugins can extend this interface via module augmentation to add their own state.\n *\n * Used with `grid.getColumnState()` and `grid.columnState` for persisting\n * user customizations (column widths, order, visibility, sort).\n *\n * @example\n * ```typescript\n * // Save column state to localStorage\n * const state = grid.getColumnState();\n * localStorage.setItem('gridState', JSON.stringify(state));\n *\n * // Restore on page load\n * const saved = localStorage.getItem('gridState');\n * if (saved) grid.applyColumnState(JSON.parse(saved));\n *\n * // Example column state structure\n * const state: GridColumnState = {\n * columns: [\n * { field: 'name', order: 0, width: 200, hidden: false },\n * { field: 'email', order: 1, width: 300, hidden: false },\n * { field: 'phone', order: 2, hidden: true }, // Hidden column\n * ],\n * sort: { field: 'name', direction: 1 },\n * };\n * ```\n *\n * @example\n * ```typescript\n * // Plugin augmentation example (in filtering plugin)\n * declare module '@toolbox-web/grid' {\n * interface ColumnState {\n * filter?: FilterValue;\n * }\n * }\n * ```\n *\n * @see {@link GridColumnState} for the full state object\n * @since 0.1.1\n */\nexport interface ColumnState {\n /** Column field identifier */\n field: string;\n /** Position index after reordering (0-based) */\n order: number;\n /** Width in pixels (undefined = use default) */\n width?: number;\n /** Visibility state */\n visible: boolean;\n /** Sort state (undefined = not sorted). */\n sort?: ColumnSortState;\n}\n\n/**\n * Sort state for a column.\n * Used within {@link ColumnState} to track sort direction and priority.\n *\n * @see {@link ColumnState} for column state persistence\n * @see {@link SortChangeDetail} for sort change events\n * @since 0.1.1\n */\nexport interface ColumnSortState {\n /** Sort direction */\n direction: 'asc' | 'desc';\n /** Priority for multi-sort (0 = primary, 1 = secondary, etc.) */\n priority: number;\n}\n\n/**\n * Complete grid column state for persistence.\n * Contains state for all columns, including plugin-contributed properties.\n *\n * @example\n * ```typescript\n * // Save state\n * const state = grid.getColumnState();\n * localStorage.setItem('grid-state', JSON.stringify(state));\n *\n * // Restore state\n * grid.applyColumnState(JSON.parse(localStorage.getItem('grid-state')));\n * ```\n *\n * @see {@link ColumnState} for individual column state\n * @see {@link PublicGrid.getColumnState} for retrieving state\n * @since 0.1.1\n */\nexport interface GridColumnState {\n /** Array of column states. */\n columns: ColumnState[];\n}\n// #endregion\n\n// #region Public Event Detail Interfaces\n/**\n * Detail for the {@link DataGridEventMap.render | `render`} event.\n *\n * Fired once at the end of every render cycle (the single RAF flush in the\n * render scheduler), after all plugin `afterRender` hooks have run and after\n * `grid.ready()` has resolved.\n *\n * Use this when you need to act on the rendered DOM (e.g. focus the first\n * input of a freshly added row when `editing.mode === 'grid'`) without\n * resorting to `setTimeout` or double-`requestAnimationFrame` hacks.\n *\n * @example\n * ```typescript\n * // Focus the first cell's input after adding a row in full-grid edit mode\n * function addEmployee() {\n * grid.addRow({ id: crypto.randomUUID(), name: '', email: '' });\n * grid.addEventListener(\n * 'render',\n * () => {\n * const input = grid.querySelector<HTMLInputElement>(\n * '[data-row=\"0\"][data-col=\"0\"] input',\n * );\n * input?.focus();\n * },\n * { once: true },\n * );\n * }\n * ```\n *\n * @category Events\n * @since 2.15.0\n */\nexport interface RenderDetail {\n /**\n * The highest render phase that executed this cycle (see {@link RenderPhase}).\n * Use this to skip cheap scroll-only renders (`phase < RenderPhase.ROWS`)\n * if you only care about row/column model changes.\n */\n phase: RenderPhase;\n /** `true` only for the very first render after the grid was connected. */\n initial: boolean;\n /** Number of rows in the effective row model after plugin `processRows` hooks ran. */\n rowCount: number;\n /**\n * The visible virtual window — `start` inclusive, `end` exclusive — or `null`\n * when virtualization is disabled (small datasets below the bypass threshold).\n *\n * When virtualization is enabled but no rows are visible (e.g. empty dataset)\n * this is `{ start: 0, end: 0 }`, NOT `null` — that lets consumers distinguish\n * \"virtualization disabled\" from \"enabled but currently empty\".\n */\n visibleRange: { start: number; end: number } | null;\n}\n\n/**\n * Detail for a cell click event.\n * Provides full context about the clicked cell including row data.\n *\n * @example\n * ```typescript\n * grid.on('cell-click', ({ row, field, value, rowIndex, colIndex }) => {\n * console.log(`Clicked ${field} = ${value} in row ${rowIndex}`);\n *\n * // Access the full row data\n * if (row.status === 'pending') {\n * showApprovalDialog(row);\n * }\n * });\n * ```\n *\n * @category Events\n * @since 1.0.0\n */\nexport interface CellClickDetail<TRow = unknown> {\n /** Zero-based row index of the clicked cell. */\n rowIndex: number;\n /** Zero-based column index of the clicked cell. */\n colIndex: number;\n /** Column configuration object for the clicked cell. */\n column: ColumnConfig<TRow>;\n /** Field name of the clicked column. */\n field: string;\n /** Cell value at the clicked position. */\n value: unknown;\n /** Full row data object. */\n row: TRow;\n /** The clicked cell element. */\n cellEl: HTMLElement;\n /** The original mouse event. */\n originalEvent: MouseEvent;\n}\n\n/**\n * Detail for a row click event.\n * Provides context about the clicked row.\n *\n * @example\n * ```typescript\n * grid.on('row-click', ({ row, rowIndex, rowEl }) => {\n * console.log(`Clicked row ${rowIndex}: ${row.name}`);\n *\n * // Highlight the row\n * rowEl.classList.add('selected');\n *\n * // Open detail panel\n * showDetailPanel(row);\n * });\n * ```\n *\n * @category Events\n * @since 1.0.0\n */\nexport interface RowClickDetail<TRow = unknown> {\n /** Zero-based row index of the clicked row. */\n rowIndex: number;\n /** Full row data object. */\n row: TRow;\n /** The clicked row element. */\n rowEl: HTMLElement;\n /** The original mouse event. */\n originalEvent: MouseEvent;\n}\n\n/**\n * Detail for a sort change (direction 0 indicates cleared sort).\n *\n * @example\n * ```typescript\n * grid.on('sort-change', ({ field, direction }) => {\n * if (direction === 0) {\n * console.log(`Sort cleared on ${field}`);\n * } else {\n * const dir = direction === 1 ? 'ascending' : 'descending';\n * console.log(`Sorted by ${field} ${dir}`);\n * }\n *\n * // Fetch sorted data from server\n * fetchData({ sortBy: field, sortDir: direction });\n * });\n * ```\n *\n * @see {@link SortState} for the sort state object\n * @see {@link SortHandler} for custom sort handlers\n * @category Events\n * @since 0.1.1\n */\nexport interface SortChangeDetail {\n /** Sorted field key. */\n field: string;\n /** Direction: 1 ascending, -1 descending, 0 cleared. */\n direction: 1 | -1 | 0;\n}\n\n/**\n * Column resize event detail containing final pixel width.\n *\n * @example\n * ```typescript\n * grid.on('column-resize', ({ field, width }) => {\n * console.log(`Column ${field} resized to ${width}px`);\n *\n * // Persist to user preferences\n * saveColumnWidth(field, width);\n * });\n * ```\n *\n * @see {@link ColumnState} for persisting column state\n * @see {@link ResizeController} for resize implementation\n * @category Events\n * @since 0.1.1\n */\nexport interface ColumnResizeDetail {\n /** Resized column field key. */\n field: string;\n /** New width in pixels. */\n width: number;\n}\n\n/**\n * Column resize-reset event detail.\n *\n * Fired when a user-resized column is restored to its original configured width\n * (e.g., via the column header context menu \"Reset width\" action). The `width`\n * field reflects the column's `__originalWidth` and may be `undefined` if the\n * column was originally auto-sized.\n *\n * @example\n * ```typescript\n * grid.on('column-resize-reset', ({ field, width }) => {\n * if (width === undefined) {\n * console.log(`Column ${field} restored to auto-size`);\n * } else {\n * console.log(`Column ${field} restored to ${width}px`);\n * }\n * });\n * ```\n *\n * @see {@link ColumnResizeDetail} for the resize-in-progress event\n * @category Events\n * @since 2.6.0\n */\nexport interface ColumnResizeResetDetail {\n /** Reset column field key. */\n field: string;\n /** Original configured width in pixels, or `undefined` if auto-sized. */\n width: number | undefined;\n}\n\n/**\n * Trigger type for cell activation.\n * - `'keyboard'`: Enter key pressed on focused cell\n * - `'pointer'`: Mouse/touch/pen click on cell\n *\n * @see {@link CellActivateDetail} for the activation event detail\n * @category Events\n * @since 1.0.0\n */\nexport type CellActivateTrigger = 'keyboard' | 'pointer';\n\n/**\n * Fired when a cell is activated by user interaction (Enter key or click).\n * Unified event for both keyboard and pointer activation.\n *\n * @example\n * ```typescript\n * grid.on('cell-activate', ({ row, field, value, trigger, cellEl }, event) => {\n * if (trigger === 'keyboard') {\n * console.log('Activated via Enter key');\n * } else {\n * console.log('Activated via click/tap');\n * }\n *\n * // Start custom editing for specific columns\n * if (field === 'notes') {\n * event.preventDefault(); // Prevent default editing\n * openNotesEditor(row, cellEl);\n * }\n * });\n * ```\n *\n * @see {@link CellClickDetail} for click-only events\n * @see {@link CellActivateTrigger} for trigger types\n * @category Events\n * @since 1.0.0\n */\nexport interface CellActivateDetail<TRow = unknown> {\n /** Zero-based row index of the activated cell. */\n rowIndex: number;\n /** Zero-based column index of the activated cell. */\n colIndex: number;\n /** Field name of the activated column. */\n field: string;\n /** Cell value at the activated position. */\n value: unknown;\n /** Full row data object. */\n row: TRow;\n /** The activated cell element. */\n cellEl: HTMLElement;\n /** What triggered the activation. */\n trigger: CellActivateTrigger;\n /** The original event (KeyboardEvent for keyboard, MouseEvent/PointerEvent for pointer). */\n originalEvent: KeyboardEvent | MouseEvent | PointerEvent;\n}\n\n/**\n * Event detail for mounting external view renderers.\n *\n * Emitted when a cell uses an external component spec (React, Angular, Vue)\n * and needs the framework adapter to mount the component.\n *\n * @example\n * ```typescript\n * // Framework adapter listens for this event\n * grid.on('mount-external-view', ({ placeholder, spec, context }) => {\n * // Mount framework component into placeholder\n * mountComponent(spec.component, placeholder, context);\n * });\n * ```\n *\n * @see {@link ColumnConfig.externalView} for external view spec\n * @see {@link FrameworkAdapter} for adapter interface\n * @category Framework Adapters\n * @since 0.1.1\n */\nexport interface ExternalMountViewDetail<TRow = unknown> {\n placeholder: HTMLElement;\n spec: unknown;\n context: { row: TRow; value: unknown; field: string; column: unknown };\n}\n\n/**\n * Event detail for mounting external editor renderers.\n *\n * Emitted when a cell uses an external editor component spec and needs\n * the framework adapter to mount the editor with commit/cancel bindings.\n *\n * @example\n * ```typescript\n * // Framework adapter listens for this event\n * grid.on('mount-external-editor', ({ placeholder, spec, context }) => {\n * // Mount framework editor with commit/cancel wired\n * mountEditor(spec.component, placeholder, {\n * value: context.value,\n * onCommit: context.commit,\n * onCancel: context.cancel,\n * });\n * });\n * ```\n *\n * @see {@link ColumnEditorSpec} for external editor spec\n * @see {@link FrameworkAdapter} for adapter interface\n * @category Framework Adapters\n * @since 0.1.1\n */\nexport interface ExternalMountEditorDetail<TRow = unknown> {\n placeholder: HTMLElement;\n spec: unknown;\n context: {\n row: TRow;\n value: unknown;\n field: string;\n column: unknown;\n commit: (v: unknown) => void;\n cancel: () => void;\n };\n}\n\n/**\n * Maps event names to their detail payload types.\n *\n * Used by {@link DataGridElement.on | grid.on()} and `addEventListener()` for\n * fully typed event handling. Plugins extend this map via module augmentation.\n *\n * @example\n * ```typescript\n * // Recommended: grid.on() auto-unwraps the detail\n * const off = grid.on('cell-click', ({ field, value, row }) => {\n * console.log(`Clicked ${field} = ${value}`);\n * });\n * off(); // unsubscribe\n *\n * // addEventListener works too (useful for { once, signal, capture })\n * grid.addEventListener('cell-click', (e) => {\n * console.log(e.detail.field);\n * }, { once: true });\n * ```\n *\n * @see {@link DataGridElement.on} for the recommended subscription API\n * @see {@link DataGridCustomEvent} for typed CustomEvent wrapper\n * @category Events\n * @since 0.1.1\n */\nexport interface DataGridEventMap<TRow = unknown> {\n /**\n * Fired when a cell is clicked.\n * Provides full context: row data, column config, cell element, and the original mouse event.\n *\n * @example\n * ```typescript\n * grid.on('cell-click', ({ row, field, value, cellEl }) => {\n * console.log(`Clicked ${field} = ${value}`);\n *\n * // Open a detail dialog for a specific column\n * if (field === 'avatar') {\n * showImagePreview(row.avatarUrl, cellEl);\n * }\n * });\n * ```\n *\n * @see {@link CellClickDetail}\n * @group Core Events\n */\n 'cell-click': CellClickDetail<TRow>;\n\n /**\n * Fired when a row is clicked (anywhere on the row).\n * Use for row-level actions like opening a detail panel or navigating.\n *\n * @example\n * ```typescript\n * grid.on('row-click', ({ row, rowIndex }) => {\n * console.log(`Row ${rowIndex}: ${row.name}`);\n *\n * // Navigate to detail page\n * router.navigate(`/employees/${row.id}`);\n * });\n * ```\n *\n * @see {@link RowClickDetail}\n * @group Core Events\n */\n 'row-click': RowClickDetail<TRow>;\n\n /**\n * Fired when a cell is activated by Enter key or pointer click.\n * Unified event for both keyboard and pointer activation.\n *\n * Call `event.preventDefault()` to suppress default behavior (e.g., inline editing).\n *\n * @example\n * ```typescript\n * grid.on('cell-activate', ({ row, field, trigger, cellEl }, event) => {\n * // Custom editing for a specific column\n * if (field === 'notes') {\n * event.preventDefault();\n * openRichTextEditor(row, cellEl);\n * }\n *\n * console.log(`Activated via ${trigger}`); // 'keyboard' | 'pointer'\n * });\n * ```\n *\n * @see {@link CellActivateDetail}\n * @see {@link CellActivateTrigger}\n * @group Core Events\n */\n 'cell-activate': CellActivateDetail<TRow>;\n\n /**\n * Fired after any data mutation — user edits, cascade updates, or API calls.\n * This is an informational event for logging, auditing, or cascading updates\n * to related fields. Check `source` to distinguish edit origins.\n *\n * @example\n * ```typescript\n * grid.on('cell-change', ({ row, rowId, field, oldValue, newValue, source }) => {\n * console.log(`${field}: ${oldValue} → ${newValue} (${source})`);\n *\n * // Cascade: recalculate total when quantity changes\n * if (source === 'user' && field === 'quantity') {\n * grid.updateRow(rowId, { total: newValue * row.price });\n * }\n * });\n * ```\n *\n * @see {@link CellChangeDetail}\n * @see {@link UpdateSource}\n * @group Core Events\n */\n 'cell-change': CellChangeDetail<TRow>;\n\n /**\n * Fired whenever the grid's row data changes — new data assignment,\n * row insertion/removal, or in-place mutations via `updateRow()`.\n * Use to keep external UI (row counts, summaries, charts) in sync.\n *\n * @example\n * ```typescript\n * grid.on('data-change', ({ rowCount, sourceRowCount }) => {\n * statusBar.textContent = `${rowCount} of ${sourceRowCount} rows`;\n * });\n * ```\n *\n * @see {@link DataChangeDetail}\n * @group Core Events\n */\n 'data-change': DataChangeDetail;\n\n /**\n * Fired when the grid's viewport is scrolled vertically (rAF-batched).\n * Use to trigger pagination, defer heavy cell content, dismiss overlays,\n * or sync external UI to the grid's scroll position.\n *\n * For server-side pagination of large datasets, prefer the\n * `ServerSidePlugin` — it handles block fetching out of the box.\n *\n * @example\n * ```typescript\n * // Infinite scroll\n * grid.on('tbw-scroll', ({ scrollTop, scrollHeight, clientHeight }) => {\n * if (scrollTop + clientHeight >= scrollHeight - 200) loadMore();\n * });\n *\n * // Defer heavy cell content (Angular @defer style)\n * grid.on('tbw-scroll', ({ scrollTop }) => {\n * updateVisibleHeavyCells(scrollTop);\n * });\n *\n * // Dismiss tooltip/popover on scroll\n * grid.on('tbw-scroll', () => closeOpenOverlays());\n * ```\n *\n * @see {@link TbwScrollDetail}\n * @group Core Events\n */\n 'tbw-scroll': TbwScrollDetail;\n\n /**\n * Emitted when a cell with an external view renderer (React, Angular, Vue component)\n * needs to be mounted. Framework adapters listen for this event internally.\n *\n * @example\n * ```typescript\n * // Custom framework adapter\n * grid.on('mount-external-view', ({ placeholder, spec, context }) => {\n * myFramework.render(spec.component, placeholder, {\n * row: context.row,\n * value: context.value,\n * });\n * });\n * ```\n *\n * @see {@link ExternalMountViewDetail}\n * @see {@link FrameworkAdapter}\n * @group Framework Adapter Events\n */\n 'mount-external-view': ExternalMountViewDetail<TRow>;\n\n /**\n * Emitted when a cell with an external editor component (React, Angular, Vue)\n * needs to be mounted with commit/cancel bindings. Framework adapters listen\n * for this event internally.\n *\n * @example\n * ```typescript\n * // Custom framework adapter\n * grid.on('mount-external-editor', ({ placeholder, spec, context }) => {\n * myFramework.render(spec.component, placeholder, {\n * value: context.value,\n * onSave: context.commit,\n * onCancel: context.cancel,\n * });\n * });\n * ```\n *\n * @see {@link ExternalMountEditorDetail}\n * @see {@link FrameworkAdapter}\n * @group Framework Adapter Events\n */\n 'mount-external-editor': ExternalMountEditorDetail<TRow>;\n\n /**\n * Fired when the sort state changes — column header click, programmatic sort,\n * or sort cleared. `direction: 0` indicates the sort was removed.\n *\n * @example\n * ```typescript\n * grid.on('sort-change', ({ field, direction }) => {\n * if (direction === 0) {\n * console.log('Sort cleared');\n * } else {\n * console.log(`Sorted by ${field} ${direction === 1 ? 'ASC' : 'DESC'}`);\n * }\n *\n * // Server-side sorting\n * fetchData({ sortBy: field, sortDir: direction });\n * });\n * ```\n *\n * @see {@link SortChangeDetail}\n * @see {@link SortHandler}\n * @group Core Events\n */\n 'sort-change': SortChangeDetail;\n\n /**\n * Fired when a column is resized by the user dragging the resize handle.\n * Use to persist column widths to user preferences or localStorage.\n *\n * @example\n * ```typescript\n * grid.on('column-resize', ({ field, width }) => {\n * console.log(`Column \"${field}\" resized to ${width}px`);\n *\n * // Persist to localStorage\n * const widths = JSON.parse(localStorage.getItem('col-widths') ?? '{}');\n * widths[field] = width;\n * localStorage.setItem('col-widths', JSON.stringify(widths));\n * });\n * ```\n *\n * @see {@link ColumnResizeDetail}\n * @group Core Events\n */\n 'column-resize': ColumnResizeDetail;\n\n /**\n * Fired when a user-resized column is reset to its original configured width.\n * Triggered by the column header context menu \"Reset width\" action.\n *\n * @example\n * ```typescript\n * grid.on('column-resize-reset', ({ field, width }) => {\n * const widths = JSON.parse(localStorage.getItem('col-widths') ?? '{}');\n * delete widths[field];\n * localStorage.setItem('col-widths', JSON.stringify(widths));\n * });\n * ```\n *\n * @see {@link ColumnResizeResetDetail}\n * @group Core Events\n */\n 'column-resize-reset': ColumnResizeResetDetail;\n\n /**\n * Fired when column state changes — reordering, resizing, visibility toggle,\n * or sort changes. Use with `getColumnState()` / `columnState` setter for\n * full state persistence.\n *\n * @example\n * ```typescript\n * grid.on('column-state-change', (state) => {\n * localStorage.setItem('grid-state', JSON.stringify(state));\n * console.log(`${state.columns.length} columns in state`);\n * });\n *\n * // Restore on load\n * const saved = localStorage.getItem('grid-state');\n * if (saved) grid.applyColumnState(JSON.parse(saved));\n * ```\n *\n * @see {@link GridColumnState}\n * @see {@link ColumnState}\n * @group Core Events\n */\n 'column-state-change': GridColumnState;\n\n /**\n * Fired once at the end of every render cycle, after all plugin\n * `afterRender` hooks have run and `ready()` has resolved.\n *\n * This is the canonical post-render hook for consumers. Use it instead of\n * `setTimeout` / double-`requestAnimationFrame` to act on the rendered DOM\n * after a programmatic mutation.\n *\n * Note: `ready()` only resolves once (after the first render). The `render`\n * event fires on every flush — including scroll-driven virtual-window\n * updates — so prefer `{ once: true }` (or check `detail.phase`) when you\n * want to act on a specific mutation.\n *\n * @example\n * ```typescript\n * // Focus the first cell's input after addRow in full-grid edit mode\n * grid.addRow({ id: crypto.randomUUID(), name: '', email: '' });\n * grid.addEventListener(\n * 'render',\n * () => {\n * const input = grid.querySelector<HTMLInputElement>(\n * '[data-row=\"0\"][data-col=\"0\"] input',\n * );\n * input?.focus();\n * },\n * { once: true },\n * );\n * ```\n *\n * @see {@link RenderDetail}\n * @group Core Events\n * @since 2.15.0\n */\n render: RenderDetail;\n\n // Note: 'cell-commit', 'row-commit', 'changed-rows-reset' are added via\n // module augmentation by EditingPlugin when imported\n}\n\n/**\n * Extracts the event detail type for a given event name.\n *\n * Utility type for getting the detail payload type of a specific event.\n *\n * @example\n * ```typescript\n * // Extract detail type for specific event\n * type ClickDetail = DataGridEventDetail<'cell-click', Employee>;\n * // Equivalent to: CellClickDetail<Employee>\n *\n * // Use in generic handler\n * function logDetail<K extends keyof DataGridEventMap>(\n * eventName: K,\n * detail: DataGridEventDetail<K>,\n * ): void {\n * console.log(`${eventName}:`, detail);\n * }\n * ```\n *\n * @see {@link DataGridEventMap} for all event types\n * @category Events\n */\nexport type DataGridEventDetail<K extends keyof DataGridEventMap<unknown>, TRow = unknown> = DataGridEventMap<TRow>[K];\n\n/**\n * Custom event type for DataGrid events with typed detail payload.\n *\n * Primarily useful when you need to declare handler parameters with\n * `addEventListener`. For most use cases, prefer {@link DataGridElement.on | grid.on()}\n * which handles typing automatically.\n *\n * @example\n * ```typescript\n * // Typed handler for addEventListener\n * function onCellClick(e: DataGridCustomEvent<'cell-click', Employee>): void {\n * const { row, field, value } = e.detail;\n * console.log(`Clicked ${field} = ${value} on ${row.name}`);\n * }\n * grid.addEventListener('cell-click', onCellClick);\n *\n * // With grid.on() you don't need this type — it's inferred:\n * grid.on('cell-click', ({ row, field, value }) => {\n * console.log(`Clicked ${field} = ${value} on ${row.name}`);\n * });\n * ```\n *\n * @see {@link DataGridElement.on} for the recommended subscription API\n * @see {@link DataGridEventMap} for all event types\n * @see `DataGridEventDetail` for extracting detail type only\n * @category Events\n * @since 0.1.1\n */\nexport type DataGridCustomEvent<K extends keyof DataGridEventMap<unknown>, TRow = unknown> = CustomEvent<\n DataGridEventMap<TRow>[K]\n>;\n\n/**\n * Template evaluation context for dynamic templates.\n *\n * @category Plugin Development\n * @since 0.1.1\n */\nexport interface EvalContext {\n value: unknown;\n row: Record<string, unknown> | null;\n}\n// #endregion\n","/**\n * Base Grid Plugin Class\n *\n * All plugins extend this abstract class.\n * Plugins are instantiated per-grid and manage their own state.\n */\n\n// Injected by Vite at build time from package.json (same as grid.ts)\ndeclare const __GRID_VERSION__: string;\n\nimport type { HeaderContentDefinition, ToolPanelDefinition } from '../../plugins/shell/types';\nimport {\n type DiagnosticCode,\n PLUGIN_ALIAS_CONFIG_CONFLICT,\n formatDiagnostic,\n gridPrefix,\n} from '../internal/diagnostics';\nimport { sanitizeHTML } from '../internal/sanitize';\nimport type { ColumnConfig, ColumnState, GridConfig, GridIcons, GridPlugin, IconValue, PluginNameMap } from '../types';\nimport { DEFAULT_GRID_ICONS } from '../types';\n\n// Re-export shared plugin types for convenience\nexport type {\n AfterCellRenderContext,\n AfterRowRenderContext,\n CellClickEvent,\n CellCoords,\n CellEditor,\n CellMouseEvent,\n CellRenderer,\n ContextMenuItem,\n ContextMenuParams,\n GridElementRef,\n HeaderClickEvent,\n HeaderRenderer,\n KeyboardModifiers,\n PluginCellRenderContext,\n PluginQuery,\n RowClickEvent,\n ScrollEvent,\n} from './types';\n\nimport type {\n AfterCellRenderContext,\n AfterRowRenderContext,\n CellClickEvent,\n CellEditor,\n CellMouseEvent,\n CellRenderer,\n GridElementRef,\n HeaderClickEvent,\n HeaderRenderer,\n PluginQuery,\n RowClickEvent,\n ScrollEvent,\n} from './types';\n\n/**\n * Grid element interface for plugins.\n * Extends GridElementRef with plugin-specific methods.\n * Note: effectiveConfig is already available from GridElementRef.\n */\nexport interface GridElement extends GridElementRef {\n getPlugin<T extends BaseGridPlugin>(PluginClass: new (...args: any[]) => T): T | undefined;\n getPluginByName<K extends string>(\n name: K,\n ): (K extends keyof PluginNameMap ? PluginNameMap[K] : BaseGridPlugin) | undefined;\n /**\n * Get a plugin's state by plugin name.\n * This is a loose-coupling method for plugins to access other plugins' state\n * without importing the plugin class directly.\n * @internal Plugin API\n */\n getPluginState?(name: string): unknown;\n\n // Focus Management\n /** Register an external DOM element as a logical focus container. @internal Plugin API */\n registerExternalFocusContainer?(el: Element): void;\n /** Unregister a previously registered external focus container. @internal Plugin API */\n unregisterExternalFocusContainer?(el: Element): void;\n /** Check whether focus is logically inside this grid (own DOM + external containers). @internal Plugin API */\n containsFocus?(node?: Node | null): boolean;\n}\n\n/**\n * Header render context for plugin header renderers.\n */\nexport interface PluginHeaderRenderContext {\n /** Column configuration */\n column: ColumnConfig;\n /** Column index */\n colIndex: number;\n}\n\n// ============================================================================\n// Plugin Dependency Types\n// ============================================================================\n\n/**\n * Declares a dependency on another plugin.\n *\n * @category Plugin Development\n * @example\n * ```typescript\n * export class UndoRedoPlugin extends BaseGridPlugin {\n * static readonly dependencies: PluginDependency[] = [\n * { name: 'editing', required: true },\n * ];\n * }\n * ```\n * @since 0.4.1\n */\nexport interface PluginDependency {\n /**\n * The name of the required plugin (matches the plugin's `name` property).\n * Use string names for loose coupling - avoids static imports.\n */\n name: string;\n\n /**\n * Whether this dependency is required (hard) or optional (soft).\n * - `true`: Plugin cannot function without it. Throws error if missing.\n * - `false`: Plugin works with reduced functionality. Silent if missing\n * (set {@link severity} to `'warn'` or `'info'` to opt into a message).\n * @default true\n */\n required?: boolean;\n\n /**\n * Explicit severity when this dependency is missing. Overrides the default\n * derived from {@link required}.\n * - `'error'`: throw (halts grid setup). Default when `required` is not `false`.\n * - `'warn'`: log a console warning and continue (development only).\n * - `'info'`: log a verbose console.debug message and continue (development only).\n *\n * When omitted, a missing hard dependency throws and a missing soft\n * dependency is silent — preserving backward-compatible behavior.\n * @since 2.16.0\n */\n severity?: 'error' | 'warn' | 'info';\n\n /**\n * Optional predicate that gates whether this dependency applies, evaluated\n * against the depending plugin's own resolved configuration (defaults merged\n * with user config). Return `false` to skip the dependency entirely.\n *\n * Lets a plugin require/recommend another plugin only under certain config\n * — e.g. a plugin that needs a tool-panel host only when its tool panel is\n * enabled. When omitted, the dependency always applies.\n * @example when: (cfg: PivotConfig) => cfg.showToolPanel === true\n * @since 2.16.0\n */\n when?: (pluginConfig: unknown) => boolean;\n\n /**\n * Human-readable reason for this dependency.\n * Shown in error/info messages to help users understand why.\n * @example \"UndoRedoPlugin needs EditingPlugin to track cell edits\"\n */\n reason?: string;\n}\n\n/**\n * Declares an incompatibility between plugins.\n * When both plugins are loaded, a warning is logged to help users understand the conflict.\n *\n * @category Plugin Development\n */\nexport interface PluginIncompatibility {\n /**\n * The name of the incompatible plugin (matches the plugin's `name` property).\n */\n name: string;\n\n /**\n * Human-readable reason for the incompatibility.\n * Should explain why the plugins conflict and any workarounds.\n * @example \"Both transform the entire row model in different ways\"\n */\n reason: string;\n}\n\n// ============================================================================\n// Plugin Query & Event Definitions\n// ============================================================================\n\n/**\n * Defines a query that a plugin can handle.\n * Other plugins or the grid can send this query type to retrieve data.\n *\n * @category Plugin Development\n *\n * @example\n * ```typescript\n * // In manifest\n * queries: [\n * {\n * type: 'canMoveColumn',\n * description: 'Check if a column can be moved/reordered',\n * },\n * ]\n *\n * // In plugin class\n * handleQuery(query: PluginQuery): unknown {\n * if (query.type === 'canMoveColumn') {\n * return this.canMoveColumn(query.context);\n * }\n * }\n * ```\n * @since 1.8.0\n */\nexport interface QueryDefinition {\n /**\n * The query type identifier (e.g., 'canMoveColumn', 'getContextMenuItems').\n * Should be unique across all plugins.\n */\n type: string;\n\n /**\n * Human-readable description of what the query does.\n */\n description?: string;\n}\n\n/**\n * Defines an event that a plugin can emit.\n * Other plugins can subscribe to these events via the Event Bus.\n *\n * @category Plugin Development\n *\n * @example\n * ```typescript\n * // In manifest\n * events: [\n * {\n * type: 'filter-change',\n * description: 'Emitted when filter criteria change',\n * },\n * ]\n *\n * // In plugin class - emit\n * this.emitPluginEvent('filter-change', { field: 'name', value: 'Alice' });\n *\n * // In another plugin - subscribe\n * this.on('filter-change', (detail) => console.log('Filter changed:', detail));\n * ```\n * @since 1.8.0\n */\nexport interface EventDefinition {\n /**\n * The event type identifier (e.g., 'filter-change', 'selection-change').\n * Used when emitting and subscribing to events.\n */\n type: string;\n\n /**\n * Human-readable description of what the event represents.\n */\n description?: string;\n\n /**\n * Whether this event is cancelable via `event.preventDefault()`.\n * @default false\n */\n cancelable?: boolean;\n}\n\n// ============================================================================\n// Plugin Manifest Types\n// ============================================================================\n\n/**\n * Defines a property that a plugin \"owns\" - used for configuration validation.\n * When this property is used without the owning plugin loaded, an error is thrown.\n *\n * @category Plugin Development\n */\nexport interface PluginPropertyDefinition {\n /** The property name on column or grid config */\n property: string;\n /** Whether this is a column-level or config-level property */\n level: 'column' | 'config';\n /** Human-readable description for error messages (e.g., 'the \"editable\" column property') */\n description: string;\n /** Import path hint for error messages (e.g., \"import { EditingPlugin } from '@toolbox-web/grid/plugins/editing';\") */\n importHint?: string;\n /** Custom check for whether property is considered \"used\" (default: truthy value check) */\n isUsed?: (value: unknown) => boolean;\n}\n\n/**\n * A configuration validation rule for detecting invalid/conflicting settings.\n * Plugins declare rules in their manifest; the validator executes them during grid initialization.\n *\n * @category Plugin Development\n * @template TConfig - The plugin's configuration type\n */\nexport interface PluginConfigRule<TConfig = unknown> {\n /** Rule identifier for debugging (e.g., 'selection/range-dblclick') */\n id: string;\n /** Severity: 'error' throws, 'warn' logs warning */\n severity: 'error' | 'warn';\n /** Human-readable message shown when rule is violated */\n message: string;\n /** Predicate returning true if the rule is VIOLATED (i.e., config is invalid) */\n check: (pluginConfig: TConfig) => boolean;\n}\n\n/**\n * Hook names that can have execution priority configured.\n *\n * @category Plugin Development\n */\nexport type HookName =\n | 'processColumns'\n | 'processConfig'\n | 'processRows'\n | 'afterStructuralRender'\n | 'afterRender'\n | 'afterCellRender'\n | 'afterRowRender'\n | 'onCellClick'\n | 'onCellMouseDown'\n | 'onCellMouseMove'\n | 'onCellMouseUp'\n | 'onHeaderClick'\n | 'onKeyDown'\n | 'onRowClick'\n | 'onScroll'\n | 'onScrollRender';\n\n/**\n * Static metadata about a plugin's capabilities and requirements.\n * Declared as a static property on plugin classes.\n *\n * @category Plugin Development\n * @template TConfig - The plugin's configuration type\n *\n * @example\n * ```typescript\n * export class MyPlugin extends BaseGridPlugin<MyConfig> {\n * static override readonly manifest: PluginManifest<MyConfig> = {\n * ownedProperties: [\n * { property: 'myProp', level: 'column', description: 'the \"myProp\" column property' },\n * ],\n * configRules: [\n * { id: 'my-plugin/invalid-combo', severity: 'warn', message: '...', check: (c) => c.a && c.b },\n * ],\n * };\n * readonly name = 'myPlugin';\n * }\n * ```\n * @since 1.1.0\n */\nexport interface PluginManifest<TConfig = unknown> {\n /**\n * Properties this plugin owns - validated by validate-config.ts.\n * If a user uses one of these properties without loading the plugin, an error is thrown.\n */\n ownedProperties?: PluginPropertyDefinition[];\n\n /**\n * Hook execution priority (higher = later, default 0).\n * Use negative values to run earlier, positive to run later.\n */\n hookPriority?: Partial<Record<HookName, number>>;\n\n /**\n * Configuration validation rules - checked during grid initialization.\n * Rules with severity 'error' throw, 'warn' logs to console.\n */\n configRules?: PluginConfigRule<TConfig>[];\n\n /**\n * Plugins that are incompatible with this plugin.\n * When both plugins are loaded together, a warning is shown.\n *\n * @example\n * ```typescript\n * incompatibleWith: [\n * { name: 'tree', reason: 'Both transform the entire row model in different ways' },\n * ],\n * ```\n */\n incompatibleWith?: PluginIncompatibility[];\n\n /**\n * Queries this plugin can handle.\n * Declares what query types this plugin responds to via `handleQuery()`.\n * This replaces the centralized PLUGIN_QUERIES approach with manifest-declared queries.\n *\n * @example\n * ```typescript\n * queries: [\n * { type: 'canMoveColumn', description: 'Check if a column can be moved' },\n * ],\n * ```\n */\n queries?: QueryDefinition[];\n\n /**\n * Events this plugin can emit.\n * Declares what event types other plugins can subscribe to via `on()`.\n *\n * @example\n * ```typescript\n * events: [\n * { type: 'filter-change', description: 'Emitted when filter criteria change' },\n * ],\n * ```\n */\n events?: EventDefinition[];\n\n /**\n * Whether this plugin's `processRows` hook injects or removes rows\n * (group headers, tree nodes, pivot aggregates, placeholders, etc.).\n *\n * When true, the core sorting module delegates to the render scheduler\n * instead of sorting `_rows` in-place, because `_rows` may contain\n * plugin-generated marker objects that would be corrupted by a direct sort.\n *\n * Plugins that only **filter** or **reorder** existing rows (e.g. Filtering,\n * MultiSort) should leave this unset or set it to `false`.\n */\n modifiesRowStructure?: boolean;\n}\n\n/** Convert camelCase icon key to kebab-case data-icon attribute value. */\nexport function toIconAttr(key: string): string {\n return key.replace(/([A-Z])/g, '-$1').toLowerCase();\n}\n\n/**\n * Abstract base class for all grid plugins.\n *\n * @category Plugin Development\n * @template TConfig - Configuration type for the plugin\n * @since 0.1.1\n */\nexport abstract class BaseGridPlugin<TConfig = unknown> implements GridPlugin {\n /**\n * Plugin dependencies - declare other plugins this one requires.\n *\n * Dependencies are validated when the plugin is attached.\n * Required dependencies throw an error if missing. Optional dependencies are\n * silent by default; set a {@link PluginDependency.severity} of `'warn'` or\n * `'info'` to opt into a development-only log.\n *\n * @example\n * ```typescript\n * static readonly dependencies: PluginDependency[] = [\n * { name: 'editing', required: true, reason: 'Tracks cell edits for undo/redo' },\n * { name: 'selection', required: false, reason: 'Enables selection-based undo' },\n * ];\n * ```\n */\n static readonly dependencies?: PluginDependency[];\n\n /**\n * Plugin manifest - declares owned properties, config rules, and hook priorities.\n *\n * This is read by the configuration validator to:\n * - Validate that required plugins are loaded when their properties are used\n * - Execute configRules to detect invalid/conflicting settings\n * - Order hook execution based on priority\n *\n * @example\n * ```typescript\n * static override readonly manifest: PluginManifest<MyConfig> = {\n * ownedProperties: [\n * { property: 'myProp', level: 'column', description: 'the \"myProp\" column property' },\n * ],\n * configRules: [\n * { id: 'myPlugin/conflict', severity: 'warn', message: '...', check: (c) => c.a && c.b },\n * ],\n * };\n * ```\n */\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n static readonly manifest?: PluginManifest<any>;\n\n /** Unique plugin identifier (derived from class name by default) */\n abstract readonly name: string;\n\n /**\n * Alternative names for backward compatibility.\n * `getPluginByName()` matches against both `name` and `aliases`.\n * @internal\n */\n readonly aliases?: readonly string[];\n\n /**\n * Plugin version - defaults to grid version for built-in plugins.\n * Third-party plugins can override with their own semver.\n */\n readonly version: string = typeof __GRID_VERSION__ !== 'undefined' ? __GRID_VERSION__ : 'dev';\n\n /** CSS styles to inject via `document.adoptedStyleSheets` */\n readonly styles?: string;\n\n /** Custom cell renderers keyed by type name */\n readonly cellRenderers?: Record<string, CellRenderer>;\n\n /** Custom header renderers keyed by type name */\n readonly headerRenderers?: Record<string, HeaderRenderer>;\n\n /** Custom cell editors keyed by type name */\n readonly cellEditors?: Record<string, CellEditor>;\n\n /** The grid instance this plugin is attached to */\n protected grid!: GridElement;\n\n /** Plugin configuration - merged with defaults in attach() */\n protected config!: TConfig;\n\n /** User-provided configuration from constructor */\n protected readonly userConfig: Partial<TConfig>;\n\n /**\n * Plugin-level AbortController for event listener cleanup.\n * Created fresh in attach(), aborted in detach().\n * This ensures event listeners are properly cleaned up when plugins are re-attached.\n */\n #abortController?: AbortController;\n\n /**\n * Default configuration - subclasses should override this getter.\n * Note: This must be a getter (not property initializer) for proper inheritance\n * since property initializers run after parent constructor.\n */\n protected get defaultConfig(): Partial<TConfig> {\n return {};\n }\n\n constructor(config: Partial<TConfig> = {}) {\n this.userConfig = config;\n }\n\n /**\n * Effective configuration (defaults merged with user config), resolved\n * eagerly so it is available **before** {@link attach} runs. Dependency\n * validation happens before `attach()` merges {@link config}, so config\n * -conditional dependency `when` predicates evaluate against this getter.\n *\n * While attached, returns the already-merged {@link config}; otherwise\n * recomputes `defaultConfig` + `userConfig` fresh. `config` is intentionally\n * **not** trusted when detached because {@link detach} does not clear it — a\n * re-validated plugin would otherwise see stale config from a prior\n * attachment. The {@link attach}-managed abort controller is the\n * attached/detached signal.\n * @internal Plugin infrastructure (used by `validatePluginDependencies`).\n */\n get resolvedConfig(): TConfig {\n return (this.#abortController ? this.config : { ...this.defaultConfig, ...this.userConfig }) as TConfig;\n }\n\n /**\n * Merge user-supplied configuration from other plugin instances into this\n * plugin's `userConfig`. Used by `PluginManager.attachAll()`'s alias-collapse\n * pre-pass: when a consumer instantiates the same plugin under multiple\n * names (e.g. `RowReorderPlugin` and `RowDragDropPlugin`, which are aliases\n * after V2.x), only one instance is attached, and the other instances'\n * configs are folded in via this method.\n *\n * Merge rules (shallow):\n *\n * - Same key, **same value** → silent.\n * - Same key, **different values** (incl. function vs function) → throws\n * a diagnostic naming the conflicting key. Conflicting callbacks\n * (`canDrag`, `canDrop`, etc.) cannot be reconciled automatically.\n * - **Disjoint keys** → merged cleanly.\n *\n * Subclasses may override to implement custom deep-merge logic, but should\n * call `super.mergeConfigsFrom(others)` to inherit the conflict-detection.\n *\n * @internal Plugin infrastructure (used by `PluginManager.attachAll`).\n * @throws Error with diagnostic code `TBW025` when configs conflict.\n */\n mergeConfigsFrom(others: readonly BaseGridPlugin<TConfig>[]): void {\n if (others.length === 0) return;\n const merged = { ...this.userConfig } as Record<string, unknown>;\n const sources: Record<string, BaseGridPlugin<TConfig>> = {};\n for (const key of Object.keys(merged)) sources[key] = this;\n for (const other of others) {\n const otherConfig = other.userConfig as Record<string, unknown>;\n for (const [key, value] of Object.entries(otherConfig)) {\n if (value === undefined) continue;\n if (!(key in merged)) {\n merged[key] = value;\n sources[key] = other;\n continue;\n }\n if (merged[key] === value) continue;\n // Conflict\n const ownerA = sources[key]?.constructor.name ?? this.constructor.name;\n const ownerB = other.constructor.name;\n const code = formatDiagnostic(\n PLUGIN_ALIAS_CONFIG_CONFLICT,\n `Cannot merge plugin configs for \"${this.name}\": conflicting value for \"${key}\" supplied by both ${ownerA} and ${ownerB}. Pass the option on a single instance, or remove the duplicate.`,\n undefined,\n this.name,\n );\n throw new Error(code);\n }\n }\n // Mutate userConfig in place so re-attach picks it up via attach().\n // `userConfig` is declared `readonly` to prevent reassigning the binding,\n // but the object's own properties are intentionally mutable for this\n // alias-collapse pre-pass. A single cast to a record is enough — no\n // `as unknown as` double-cast needed.\n Object.assign(this.userConfig as Record<string, unknown>, merged);\n }\n\n /**\n * Replace this plugin's `userConfig` with a freshly-resolved sibling\n * instance's config. Used by the grid's per-grid feature-instance gate\n * (`#initializePlugins`): when a feature is re-resolved across a config\n * change, the **existing** instance is reused — so its accumulated state\n * survives — but it must adopt the latest `gridConfig.features.<name>`\n * value. Unlike {@link mergeConfigsFrom}, this is last-write-wins with no\n * conflict detection: the re-resolved instance is authoritative.\n *\n * `attach()` recomputes {@link config} from `userConfig`, so updating\n * `userConfig` here is enough for the reused instance to pick up the new\n * config on its next attach during the same `#initializePlugins` pass.\n *\n * @internal Plugin infrastructure (used by the grid feature gate).\n */\n refreshUserConfigFrom(other: BaseGridPlugin<TConfig>): void {\n const target = this.userConfig as Record<string, unknown>;\n // Snapshot the source BEFORE clearing `target`. A feature factory may\n // store the consumer's config object by reference (e.g. `new\n // ContextMenuPlugin(config)`), and a consumer that passes the *same* config\n // object across re-resolutions (common when `gridConfig` is a recomputed\n // Angular `computed()`) makes `this.userConfig === other.userConfig`.\n // Without this snapshot, the `delete` loop below would empty that shared\n // object first, then `Object.assign` would copy back nothing — silently\n // wiping the config and falling back to plugin defaults. (#contextMenu)\n const source = { ...(other.userConfig as Record<string, unknown>) };\n for (const key of Object.keys(target)) delete target[key];\n Object.assign(target, source);\n }\n\n /**\n * Called when the plugin is attached to a grid.\n * Override to set up event listeners, initialize state, etc.\n *\n * @example\n * ```ts\n * attach(grid: GridElement): void {\n * super.attach(grid);\n * // Set up document-level listeners with auto-cleanup\n * document.addEventListener('keydown', this.handleEscape, {\n * signal: this.disconnectSignal\n * });\n * }\n * ```\n */\n attach(grid: GridElement): void {\n // Abort any previous abort controller (in case of re-attach without detach)\n this.#abortController?.abort();\n // Create fresh abort controller for this attachment\n this.#abortController = new AbortController();\n\n this.grid = grid;\n // Merge config here (after subclass construction is complete)\n this.config = { ...this.defaultConfig, ...this.userConfig } as TConfig;\n }\n\n /**\n * Called when the plugin is detached from a grid.\n * Override to clean up event listeners, timers, etc.\n *\n * @example\n * ```ts\n * detach(): void {\n * // Clean up any state not handled by disconnectSignal\n * this.selectedRows.clear();\n * this.cache = null;\n * }\n * ```\n */\n detach(): void {\n // Abort the plugin's abort controller to clean up all event listeners\n // registered with { signal: this.disconnectSignal }\n this.#abortController?.abort();\n this.#abortController = undefined;\n // Override in subclass for additional cleanup\n }\n\n /**\n * Called when another plugin is attached to the same grid.\n * Use for inter-plugin coordination, e.g., to integrate with new plugins.\n *\n * @param name - The name of the plugin that was attached\n * @param plugin - The plugin instance (for direct access if needed)\n *\n * @example\n * ```ts\n * onPluginAttached(name: string, plugin: BaseGridPlugin): void {\n * if (name === 'selection') {\n * // Integrate with selection plugin\n * this.selectionPlugin = plugin as SelectionPlugin;\n * }\n * }\n * ```\n */\n onPluginAttached?(name: string, plugin: BaseGridPlugin): void;\n\n /**\n * Called when another plugin is detached from the same grid.\n * Use to clean up inter-plugin references.\n *\n * @param name - The name of the plugin that was detached\n *\n * @example\n * ```ts\n * onPluginDetached(name: string): void {\n * if (name === 'selection') {\n * this.selectionPlugin = undefined;\n * }\n * }\n * ```\n */\n onPluginDetached?(name: string): void;\n\n /**\n * Get another plugin instance from the same grid.\n * Use for inter-plugin communication.\n *\n * **Prefer {@link BaseGridPlugin.grid grid.getPluginByName()}** when you don't need the class import.\n *\n * @example\n * ```ts\n * // Preferred: by name\n * const selection = this.grid?.getPluginByName('selection');\n *\n * // Alternative: by class\n * const selection = this.getPlugin(SelectionPlugin);\n * if (selection) {\n * const selectedRows = selection.getSelectedRows();\n * }\n * ```\n */\n protected getPlugin<T extends BaseGridPlugin>(PluginClass: new (...args: any[]) => T): T | undefined {\n return this.grid?.getPlugin(PluginClass);\n }\n\n /**\n * Emit a custom event from the grid.\n */\n protected emit<T>(eventName: string, detail: T): void {\n this.grid?.dispatchEvent?.(new CustomEvent(eventName, { detail, bubbles: true }));\n }\n\n /**\n * Emit a cancelable custom event from the grid.\n * @returns `true` if the event was cancelled (preventDefault called), `false` otherwise\n */\n protected emitCancelable<T>(eventName: string, detail: T): boolean {\n const event = new CustomEvent(eventName, { detail, bubbles: true, cancelable: true });\n this.grid?.dispatchEvent?.(event);\n return event.defaultPrevented;\n }\n\n // =========================================================================\n // Event Bus - Plugin-to-Plugin Communication\n // =========================================================================\n\n /**\n * Subscribe to an event from another plugin.\n * The subscription is automatically cleaned up when this plugin is detached.\n *\n * @category Plugin Development\n * @param eventType - The event type to listen for (e.g., 'filter-change')\n * @param callback - The callback to invoke when the event is emitted\n *\n * @example\n * ```typescript\n * // In attach() or other initialization\n * this.on('filter-change', (detail) => {\n * console.log('Filter changed:', detail);\n * });\n * ```\n */\n protected on<T = unknown>(eventType: string, callback: (detail: T) => void): void {\n this.grid?._pluginManager?.subscribe(this, eventType, callback as (detail: unknown) => void);\n }\n\n /**\n * Unsubscribe from a plugin event.\n *\n * @category Plugin Development\n * @param eventType - The event type to stop listening for\n *\n * @example\n * ```typescript\n * this.off('filter-change');\n * ```\n */\n protected off(eventType: string): void {\n this.grid?._pluginManager?.unsubscribe(this, eventType);\n }\n\n /**\n * Emit an event to other plugins via the Event Bus.\n * This is for inter-plugin communication only; it does NOT dispatch DOM events.\n * Use `emit()` to dispatch DOM events that external consumers can listen to.\n *\n * @category Plugin Development\n * @param eventType - The event type to emit (should be declared in manifest.events)\n * @param detail - The event payload\n *\n * @example\n * ```typescript\n * // Emit to other plugins (not DOM)\n * this.emitPluginEvent('filter-change', { field: 'name', value: 'Alice' });\n *\n * // For DOM events that consumers can addEventListener to:\n * this.emit('filter-change', { field: 'name', value: 'Alice' });\n * ```\n */\n protected emitPluginEvent<T>(eventType: string, detail: T): void {\n this.grid?._pluginManager?.emitPluginEvent(eventType, detail);\n }\n\n /**\n * Emit an event to **both** the plugin Event Bus (for inter-plugin communication)\n * **and** the DOM (for external consumers via `addEventListener`).\n *\n * Use this when a state change is relevant to both other plugins and external\n * consumers. For example, `sort-change` needs to invalidate Selection (plugin bus)\n * and notify the host application (DOM event).\n *\n * @category Plugin Development\n * @param eventType - The event type to broadcast\n * @param detail - The event payload\n *\n * @example\n * ```typescript\n * // Notify both plugins and consumers of a sort change\n * this.broadcast('sort-change', { sortModel: [...this.sortModel] });\n * ```\n */\n protected broadcast<T>(eventType: string, detail: T): void {\n this.emitPluginEvent(eventType, detail);\n this.emit(eventType, detail);\n }\n\n /**\n * Request a re-render of the grid.\n * Uses ROWS phase - does NOT trigger processColumns hooks.\n */\n protected requestRender(): void {\n this.grid?.requestRender?.();\n }\n\n /**\n * Request a columns re-render of the grid.\n * Uses COLUMNS phase - triggers processColumns hooks.\n * Use this when your plugin needs to reprocess column configuration.\n */\n protected requestColumnsRender(): void {\n (this.grid as { requestColumnsRender?: () => void })?.requestColumnsRender?.();\n }\n\n /**\n * Request a re-render and restore focus styling afterward.\n * Use this when a plugin action (like expand/collapse) triggers a render\n * but needs to maintain keyboard navigation focus.\n */\n protected requestRenderWithFocus(): void {\n this.grid?.requestRenderWithFocus?.();\n }\n\n /**\n * Request a lightweight style update without rebuilding DOM.\n * Use this instead of requestRender() when only CSS classes need updating.\n */\n protected requestAfterRender(): void {\n this.grid?.requestAfterRender?.();\n }\n\n /**\n * Re-render visible rows without rebuilding the row model or recalculating geometry.\n * Use this when row data has been updated in-place (e.g., server-side block loads)\n * and only the visible viewport needs to re-render.\n */\n protected requestVirtualRefresh(): void {\n this.grid?.requestVirtualRefresh?.();\n }\n\n /**\n * Get the current rows from the grid.\n */\n protected get rows(): any[] {\n return this.grid?.rows ?? [];\n }\n\n /**\n * Get the original unfiltered/unprocessed rows from the grid.\n * Use this when you need all source data regardless of active filters.\n */\n protected get sourceRows(): any[] {\n return this.grid?.sourceRows ?? [];\n }\n\n /**\n * Get the current columns from the grid.\n */\n protected get columns(): ColumnConfig[] {\n return this.grid?.columns ?? [];\n }\n\n /**\n * Get only visible columns from the grid (excludes hidden).\n * Use this for rendering that needs to match the grid template.\n */\n protected get visibleColumns(): ColumnConfig[] {\n return this.grid?._visibleColumns ?? [];\n }\n\n /**\n * Get the grid as an HTMLElement for direct DOM operations.\n * Use sparingly - prefer the typed GridElementRef API when possible.\n *\n * @example\n * ```ts\n * const width = this.gridElement.clientWidth;\n * this.gridElement.classList.add('my-plugin-active');\n * ```\n */\n protected get gridElement(): HTMLElement {\n return this.grid?._hostElement;\n }\n\n /**\n * Get the disconnect signal for event listener cleanup.\n * This signal is aborted when the grid disconnects from the DOM.\n * Use this when adding event listeners that should be cleaned up automatically.\n *\n * Best for:\n * - Document/window-level listeners added in attach()\n * - Listeners on the grid element itself\n * - Any listener that should persist across renders\n *\n * Not needed for:\n * - Listeners on elements created in afterRender() (removed with element)\n *\n * @example\n * element.addEventListener('click', handler, { signal: this.disconnectSignal });\n * document.addEventListener('keydown', handler, { signal: this.disconnectSignal });\n */\n protected get disconnectSignal(): AbortSignal {\n // Return the plugin's own abort signal for proper cleanup on detach/re-attach\n // Falls back to grid's signal if plugin's controller not yet created\n return this.#abortController?.signal ?? this.grid?.disconnectSignal;\n }\n\n /**\n * Get the grid-level icons configuration.\n * Returns merged icons (user config + defaults).\n */\n protected get gridIcons(): Required<GridIcons> {\n const userIcons = this.grid?.gridConfig?.icons ?? {};\n return { ...DEFAULT_GRID_ICONS, ...userIcons };\n }\n\n // #region Animation Helpers\n\n /**\n * Check if animations are enabled at the grid level.\n * Respects gridConfig.animation.mode and the CSS variable set by the grid.\n *\n * Plugins should use this to skip animations when:\n * - Animation mode is 'off' or `false`\n * - User prefers reduced motion and mode is 'reduced-motion' (default)\n *\n * @example\n * ```ts\n * private get animationStyle(): 'slide' | 'fade' | false {\n * if (!this.isAnimationEnabled) return false;\n * return this.config.animation ?? 'slide';\n * }\n * ```\n */\n protected get isAnimationEnabled(): boolean {\n const mode = this.grid?.effectiveConfig?.animation?.mode ?? 'reduced-motion';\n\n // Explicit off = disabled\n if (mode === false || mode === 'off') return false;\n\n // Explicit on = always enabled\n if (mode === true || mode === 'on') return true;\n\n // reduced-motion: check CSS variable (set by grid based on media query)\n const host = this.gridElement;\n if (host) {\n const enabled = getComputedStyle(host).getPropertyValue('--tbw-animation-enabled').trim();\n return enabled !== '0';\n }\n\n return true; // Default to enabled\n }\n\n /**\n * Get the animation duration in milliseconds from CSS variable.\n * Falls back to 200ms if not set.\n *\n * Plugins can use this for their animation timing to stay consistent\n * with the grid-level animation.duration setting.\n *\n * @example\n * ```ts\n * element.animate(keyframes, { duration: this.animationDuration });\n * ```\n */\n protected get animationDuration(): number {\n const host = this.gridElement;\n if (host) {\n const durationStr = getComputedStyle(host).getPropertyValue('--tbw-animation-duration').trim();\n const parsed = parseInt(durationStr, 10);\n if (!isNaN(parsed)) return parsed;\n }\n return 200; // Default\n }\n\n // #endregion\n\n /**\n * Set an icon on an element using the CSS-first hybrid approach.\n *\n * Sets `data-icon` for CSS pseudo-element rendering and `data-expanded` for\n * expand/collapse toggle elements. Only injects DOM content when a JS override\n * is present (via `gridConfig.icons` or `pluginOverride`), which naturally\n * suppresses the CSS `::before` via the `:empty` selector.\n *\n * @param element - The element to set the icon on\n * @param iconKey - The icon key in GridIcons (e.g., 'expand', 'collapse')\n * @param pluginOverride - Optional plugin-level override (string or HTMLElement)\n */\n protected setIcon(element: HTMLElement, iconKey: keyof GridIcons, pluginOverride?: IconValue): void {\n // Set data-icon attribute for CSS targeting (always)\n element.dataset.icon = toIconAttr(iconKey);\n\n // Set data-expanded for toggle elements (tree, group, master-detail)\n if (iconKey === 'collapse') {\n element.dataset.expanded = '';\n } else if (iconKey === 'expand') {\n delete element.dataset.expanded;\n }\n\n // Resolve JS-only icon (returns undefined when CSS default should be used)\n const jsIcon = this.#resolveJsIcon(iconKey, pluginOverride);\n\n if (jsIcon !== undefined) {\n // JS override: inject content (suppresses CSS ::before via :empty)\n if (typeof jsIcon === 'string') {\n element.innerHTML = sanitizeHTML(jsIcon);\n } else if (jsIcon instanceof HTMLElement) {\n element.innerHTML = '';\n element.appendChild(jsIcon.cloneNode(true));\n }\n } else {\n // CSS path: ensure element is empty so ::before renders\n element.innerHTML = '';\n }\n }\n\n /**\n * Resolve JS-only icon override. Returns undefined if CSS default should render.\n * Only returns a value when the user explicitly set gridConfig.icons[key] or\n * the plugin passed an override.\n */\n #resolveJsIcon(iconKey: keyof GridIcons, pluginOverride?: IconValue): IconValue | undefined {\n if (pluginOverride !== undefined) return pluginOverride;\n return this.grid?.gridConfig?.icons?.[iconKey];\n }\n\n /**\n * Create or replace a sort indicator on a header cell.\n *\n * Handles the full lifecycle: removes any existing indicator, creates a new\n * `<span part=\"sort-indicator\" class=\"sort-indicator\">` with the appropriate\n * icon, sets `aria-sort` / `data-sort` attributes, and inserts it before the\n * filter button or resize handle.\n *\n * Plugins decide **which** cells and **when** — this method handles **how**.\n *\n * @param cell - The header cell element\n * @param direction - `'asc'` | `'desc'` for sorted, `null` for unsorted idle state\n * @returns The created indicator element (useful for appending badges, etc.)\n */\n protected updateSortIndicator(cell: Element, direction: 'asc' | 'desc' | null): HTMLElement {\n // Remove any existing indicator (core uses part, plugins use class)\n cell.querySelector('[part~=\"sort-indicator\"], .sort-indicator')?.remove();\n\n const indicator = document.createElement('span');\n indicator.setAttribute('part', 'sort-indicator');\n indicator.className = 'sort-indicator';\n\n if (direction) {\n cell.setAttribute('aria-sort', direction === 'asc' ? 'ascending' : 'descending');\n cell.setAttribute('data-sort', direction);\n this.setIcon(indicator, direction === 'asc' ? 'sortAsc' : 'sortDesc');\n } else {\n cell.setAttribute('aria-sort', 'none');\n cell.removeAttribute('data-sort');\n this.setIcon(indicator, 'sortNone');\n }\n\n // Insert before filter button or resize handle to maintain DOM order\n const insertBefore = cell.querySelector('.tbw-filter-btn') ?? cell.querySelector('.resize-handle');\n if (insertBefore) {\n cell.insertBefore(indicator, insertBefore);\n } else {\n cell.appendChild(indicator);\n }\n\n return indicator;\n }\n\n /**\n * Log a warning with an optional diagnostic code.\n *\n * When a diagnostic code is provided, the message is formatted with the code\n * and a link to the troubleshooting docs.\n *\n * @example\n * ```ts\n * this.warn('Something went wrong'); // plain\n * this.warn(MISSING_BREAKPOINT, 'Set a breakpoint'); // with code + docs link\n * ```\n */\n protected warn(message: string): void;\n protected warn(code: DiagnosticCode, message: string): void;\n protected warn(codeOrMessage: DiagnosticCode | string, message?: string): void {\n if (message !== undefined) {\n // Called with (code, message)\n console.warn(formatDiagnostic(codeOrMessage as DiagnosticCode, message, this.gridElement.id, this.name));\n } else {\n // Called with (message) — plain warning, no diagnostic code\n console.warn(`${gridPrefix(this.gridElement.id, this.name)} ${codeOrMessage}`);\n }\n }\n\n /**\n * Throw an error with a diagnostic code and docs link.\n * Use for configuration errors and API misuse that should halt execution.\n */\n protected throwDiagnostic(code: DiagnosticCode, message: string): never {\n throw new Error(formatDiagnostic(code, message, this.gridElement.id, this.name));\n }\n\n // #region Lifecycle Hooks\n\n /**\n * Transform rows before rendering.\n * Called during each render cycle before rows are rendered to the DOM.\n * Use this to filter, sort, or add computed properties to rows.\n *\n * @param rows - The current rows array (readonly to encourage returning a new array)\n * @returns The modified rows array to render\n *\n * @example\n * ```ts\n * processRows(rows: readonly any[]): any[] {\n * // Filter out hidden rows\n * return rows.filter(row => !row._hidden);\n * }\n * ```\n *\n * @example\n * ```ts\n * processRows(rows: readonly any[]): any[] {\n * // Add computed properties\n * return rows.map(row => ({\n * ...row,\n * _fullName: `${row.firstName} ${row.lastName}`\n * }));\n * }\n * ```\n */\n processRows?(rows: readonly any[]): any[];\n\n /**\n * Contribute to the grid's effective configuration during each config merge.\n * Called by {@link ConfigManager.merge} after the base config is assembled,\n * before it becomes `effectiveConfig`. Mutate `config` in place to inject\n * plugin-derived settings (e.g. the shell plugin folds its light-DOM /\n * API-registered state into `config.shell`).\n *\n * This is the config-pipeline counterpart to {@link processColumns} /\n * {@link processRows}: it keeps the core config merge free of any plugin\n * knowledge — each plugin owns the shape it contributes.\n *\n * @param config - The mutable base config being assembled into effectiveConfig.\n *\n * @example\n * ```ts\n * processConfig(config: GridConfig): void {\n * config.shell = { ...config.shell, header: { title: this.#title } };\n * }\n * ```\n */\n processConfig?(config: GridConfig): void;\n\n /**\n * Transform columns before rendering.\n * Called during each render cycle before column headers and cells are rendered.\n * Use this to add, remove, or modify column definitions.\n *\n * @param columns - The current columns array (readonly to encourage returning a new array)\n * @returns The modified columns array to render\n *\n * @example\n * ```ts\n * processColumns(columns: readonly ColumnConfig[]): ColumnConfig[] {\n * // Add a selection checkbox column\n * return [\n * { field: '_select', header: '', width: 40 },\n * ...columns\n * ];\n * }\n * ```\n */\n processColumns?(columns: readonly ColumnConfig[]): ColumnConfig[];\n\n /**\n * Called before each render cycle begins.\n * Use this to prepare state or cache values needed during rendering.\n *\n * **Note:** This hook is currently a placeholder for future implementation.\n * It is defined in the interface but not yet invoked by the grid's render pipeline.\n * If you need this functionality, please open an issue or contribute an implementation.\n *\n * @example\n * ```ts\n * beforeRender(): void {\n * this.visibleRowCount = this.calculateVisibleRows();\n * }\n * ```\n */\n beforeRender?(): void;\n\n /**\n * Called after each render cycle completes.\n * Use this for DOM manipulation, adding event listeners to rendered elements,\n * or applying visual effects like selection highlights.\n *\n * @example\n * ```ts\n * afterRender(): void {\n * // Apply selection styling to rendered rows\n * const rows = this.gridElement?.querySelectorAll('.data-grid-row');\n * rows?.forEach((row, i) => {\n * row.classList.toggle('selected', this.selectedRows.has(i));\n * });\n * }\n * ```\n */\n afterRender?(): void;\n\n /**\n * Called synchronously immediately after the grid (re)builds its root DOM\n * structure, before the browser has a chance to paint.\n *\n * Unlike {@link afterRender} (which runs in the scheduler's STYLE phase and\n * may be deferred), this hook fires inside the same task as the structural\n * DOM build. It is the correct place for plugins that need to wrap, relocate,\n * or augment the freshly built grid DOM without a flash of unstyled/unwrapped\n * content — e.g. a shell that moves the grid content into a chrome wrapper.\n *\n * The grid emits this hook every time it performs a full structural rebuild\n * (initial connect and structural changes), but NOT on the scroll/data hot\n * path. Implementations MUST be idempotent: a full rebuild discards any DOM a\n * previous invocation produced, so re-create it, and no-op when the desired\n * structure is already present.\n *\n * @example\n * ```ts\n * afterStructuralRender(): void {\n * const root = this.grid?._renderRoot;\n * if (!root || root.querySelector('.my-wrapper > .tbw-grid-content')) return;\n * // build wrapper, move .tbw-grid-content inside it\n * }\n * ```\n */\n afterStructuralRender?(): void;\n\n /**\n * Called synchronously by the grid immediately before it tears down and\n * rebuilds its root DOM structure (a structural re-render). Plugins that\n * relocated existing light-DOM nodes into their own chrome (e.g. a shell that\n * moves toolbar buttons into the header bar) use this to move those nodes back\n * to their original container so they survive the rebuild. Generic\n * counterpart to {@link afterStructuralRender}. MUST be idempotent.\n */\n beforeStructuralRender?(): void;\n\n /**\n * Called by the grid after a config merge to ask whether the change requires a\n * full structural re-render (rather than a cheaper in-place update). A plugin\n * returns `true` when its rendered DOM signature no longer matches the new\n * effective config — e.g. a shell header appearing/disappearing or a tool\n * panel's count/position/mode changing. The grid performs a structural\n * re-render if ANY plugin returns `true`. MUST be side-effect free.\n */\n needsStructuralRerender?(): boolean;\n\n /**\n * Called by the grid to let a plugin (re-)parse its own light-DOM\n * contributions into plugin state ahead of a config merge. Generic seam that\n * keeps core free of plugin-specific light-DOM tag knowledge (the plugin reads\n * `this.grid._hostElement`). MUST be idempotent.\n */\n parseLightDom?(): void;\n\n /**\n * Called by the grid (from `refreshColumns` and the light-DOM observer) to\n * let a plugin re-parse its light-DOM contributions AND refresh any\n * already-rendered chrome in place when something changed — e.g. a shell\n * title or tool buttons that a framework projected asynchronously after the\n * initial render. Distinct from {@link parseLightDom}, which only updates\n * plugin state. Generic seam — core holds no plugin-specific knowledge. MUST\n * be idempotent and a no-op when the plugin's chrome is not yet rendered.\n */\n syncLightDom?(): void;\n\n /**\n * Called after each cell is rendered.\n * This hook is more efficient than `afterRender()` for cell-level modifications\n * because you receive the cell context directly - no DOM queries needed.\n *\n * Use cases:\n * - Adding selection/highlight classes to specific cells\n * - Injecting badges or decorations\n * - Applying conditional styling based on cell value\n *\n * Performance note: Called for every visible cell during render. Keep implementation fast.\n * This hook is also called during scroll when cells are recycled.\n *\n * @param context - The cell render context with row, column, value, and elements\n *\n * @example\n * ```ts\n * afterCellRender(context: AfterCellRenderContext): void {\n * // Add selection class without DOM queries\n * if (this.isSelected(context.rowIndex, context.colIndex)) {\n * context.cellElement.classList.add('selected');\n * }\n *\n * // Add validation error styling\n * if (this.hasError(context.row, context.column.field)) {\n * context.cellElement.classList.add('has-error');\n * }\n * }\n * ```\n */\n afterCellRender?(context: AfterCellRenderContext): void;\n\n /**\n * Called after a row is fully rendered (all cells complete).\n * Use this for row-level decorations, styling, or ARIA attributes.\n *\n * Common use cases:\n * - Adding selection classes to entire rows (row-focus, selected)\n * - Setting row-level ARIA attributes\n * - Applying row validation highlighting\n * - Tree indentation styling\n *\n * Performance note: Called for every visible row during render. Keep implementation fast.\n * This hook is also called during scroll when rows are recycled.\n *\n * @param context - The row render context with row data and element\n *\n * @example\n * ```ts\n * afterRowRender(context: AfterRowRenderContext): void {\n * // Add row selection class without DOM queries\n * if (this.isRowSelected(context.rowIndex)) {\n * context.rowElement.classList.add('selected', 'row-focus');\n * }\n *\n * // Add validation error styling\n * if (this.rowHasErrors(context.row)) {\n * context.rowElement.classList.add('has-errors');\n * }\n * }\n * ```\n */\n afterRowRender?(context: AfterRowRenderContext): void;\n\n /**\n * Called after scroll-triggered row rendering completes.\n * This is a lightweight hook for applying visual state to recycled DOM elements.\n * Use this instead of afterRender when you need to reapply styling during scroll.\n *\n * Performance note: This is called frequently during scroll. Keep implementation fast.\n *\n * @example\n * ```ts\n * onScrollRender(): void {\n * // Reapply selection state to visible cells\n * this.applySelectionToVisibleCells();\n * }\n * ```\n */\n onScrollRender?(): void;\n\n /**\n * Get the height of a specific row.\n * Used for synthetic rows (group headers, detail panels, etc.) that have fixed heights.\n * Return undefined if this plugin does not manage the height for this row.\n *\n * This hook is called during position cache rebuilds for variable row height virtualization.\n * Plugins that create synthetic rows should implement this to provide accurate heights.\n *\n * @param row - The row data\n * @param index - The row index in the processed rows array\n * @returns The row height in pixels, or undefined if not managed by this plugin\n */\n getRowHeight?(row: unknown, index: number): number | undefined;\n\n /**\n * Adjust the virtualization start index to render additional rows before the visible range.\n * Use this when expanded content (like detail rows) needs its parent row to remain rendered\n * even when the parent row itself has scrolled above the viewport.\n *\n * @param start - The calculated start row index\n * @param scrollTop - The current scroll position\n * @param rowHeight - The height of a single row\n * @returns The adjusted start index (lower than or equal to original start)\n *\n * @example\n * ```ts\n * adjustVirtualStart(start: number, scrollTop: number, rowHeight: number): number {\n * // If row 5 is expanded and scrolled partially, keep it rendered\n * for (const expandedRowIndex of this.expandedRowIndices) {\n * const expandedRowTop = expandedRowIndex * rowHeight;\n * const expandedRowBottom = expandedRowTop + rowHeight + this.detailHeight;\n * if (expandedRowBottom > scrollTop && expandedRowIndex < start) {\n * return expandedRowIndex;\n * }\n * }\n * return start;\n * }\n * ```\n */\n adjustVirtualStart?(start: number, scrollTop: number, rowHeight: number): number;\n\n /**\n * Render a custom row, bypassing the default row rendering.\n * Use this for special row types like group headers, detail rows, or footers.\n *\n * @param row - The row data object\n * @param rowEl - The row DOM element to render into\n * @param rowIndex - The index of the row in the data array\n * @returns `true` if the plugin handled rendering (prevents default), `false`/`void` for default rendering\n *\n * @example\n * ```ts\n * renderRow(row: any, rowEl: HTMLElement, rowIndex: number): boolean | void {\n * if (row._isGroupHeader) {\n * rowEl.innerHTML = `<div class=\"group-header\">${row._groupLabel}</div>`;\n * return true; // Handled - skip default rendering\n * }\n * // Return void to let default rendering proceed\n * }\n * ```\n */\n renderRow?(row: any, rowEl: HTMLElement, rowIndex: number): boolean | void;\n\n // #endregion\n\n // #region Inter-Plugin Communication\n\n /**\n * Handle queries from other plugins or the grid.\n *\n * Queries are declared in `manifest.queries` and dispatched via `grid.query()`.\n * This enables type-safe, discoverable inter-plugin communication.\n *\n * @category Plugin Development\n * @param query - The query object with type and context\n * @returns Query-specific response, or undefined if not handling this query\n *\n * @example\n * ```ts\n * // In manifest\n * static override readonly manifest: PluginManifest = {\n * queries: [\n * { type: 'canMoveColumn', description: 'Check if a column can be moved' },\n * ],\n * };\n *\n * // In plugin class\n * handleQuery(query: PluginQuery): unknown {\n * if (query.type === 'canMoveColumn') {\n * const column = query.context as ColumnConfig;\n * return !column.sticky; // Can't move sticky columns\n * }\n * }\n * ```\n */\n handleQuery?(query: PluginQuery): unknown;\n\n // #endregion\n\n // #region Interaction Hooks\n\n /**\n * Handle keyboard events on the grid.\n * Called when a key is pressed while the grid or a cell has focus.\n *\n * @param event - The native KeyboardEvent\n * @returns `true` to prevent default behavior and stop propagation, `false`/`void` to allow default\n *\n * @example\n * ```ts\n * onKeyDown(event: KeyboardEvent): boolean | void {\n * // Handle Ctrl+A for select all\n * if (event.ctrlKey && event.key === 'a') {\n * this.selectAllRows();\n * return true; // Prevent default browser select-all\n * }\n * }\n * ```\n */\n onKeyDown?(event: KeyboardEvent): boolean | void;\n\n /**\n * Handle cell click events.\n * Called when a data cell is clicked (not headers).\n *\n * @param event - Cell click event with row/column context\n * @returns `true` to prevent default behavior and stop propagation, `false`/`void` to allow default\n *\n * @example\n * ```ts\n * onCellClick(event: CellClickEvent): boolean | void {\n * if (event.field === '_select') {\n * this.toggleRowSelection(event.rowIndex);\n * return true; // Handled\n * }\n * }\n * ```\n */\n onCellClick?(event: CellClickEvent): boolean | void;\n\n /**\n * Handle row click events.\n * Called when any part of a data row is clicked.\n * Note: This is called in addition to onCellClick, not instead of.\n *\n * @param event - Row click event with row context\n * @returns `true` to prevent default behavior and stop propagation, `false`/`void` to allow default\n *\n * @example\n * ```ts\n * onRowClick(event: RowClickEvent): boolean | void {\n * if (this.config.mode === 'row') {\n * this.selectRow(event.rowIndex, event.originalEvent);\n * return true;\n * }\n * }\n * ```\n */\n onRowClick?(event: RowClickEvent): boolean | void;\n\n /**\n * Handle header click events.\n * Called when a column header is clicked. Commonly used for sorting.\n *\n * @param event - Header click event with column context\n * @returns `true` to prevent default behavior and stop propagation, `false`/`void` to allow default\n *\n * @example\n * ```ts\n * onHeaderClick(event: HeaderClickEvent): boolean | void {\n * if (event.column.sortable !== false) {\n * this.toggleSort(event.field);\n * return true;\n * }\n * }\n * ```\n */\n onHeaderClick?(event: HeaderClickEvent): boolean | void;\n\n /**\n * Handle scroll events on the grid viewport.\n * Called during scrolling. Note: This may be called frequently; debounce if needed.\n *\n * @param event - Scroll event with scroll position and viewport dimensions\n *\n * @example\n * ```ts\n * onScroll(event: ScrollEvent): void {\n * // Update sticky column positions\n * this.updateStickyPositions(event.scrollLeft);\n * }\n * ```\n */\n onScroll?(event: ScrollEvent): void;\n\n /**\n * Handle cell mousedown events.\n * Used for initiating drag operations like range selection or column resize.\n *\n * @param event - Mouse event with cell context\n * @returns `true` to indicate drag started (prevents text selection), `false`/`void` otherwise\n *\n * @example\n * ```ts\n * onCellMouseDown(event: CellMouseEvent): boolean | void {\n * if (event.rowIndex !== undefined && this.config.mode === 'range') {\n * this.startDragSelection(event.rowIndex, event.colIndex);\n * return true; // Prevent text selection\n * }\n * }\n * ```\n */\n onCellMouseDown?(event: CellMouseEvent): boolean | void;\n\n /**\n * Handle cell mousemove events during drag operations.\n * Only called when a drag is in progress (after mousedown returned true).\n *\n * @param event - Mouse event with current cell context\n * @returns `true` to continue handling the drag, `false`/`void` otherwise\n *\n * @example\n * ```ts\n * onCellMouseMove(event: CellMouseEvent): boolean | void {\n * if (this.isDragging && event.rowIndex !== undefined) {\n * this.extendSelection(event.rowIndex, event.colIndex);\n * return true;\n * }\n * }\n * ```\n */\n onCellMouseMove?(event: CellMouseEvent): boolean | void;\n\n /**\n * Handle cell mouseup events to end drag operations.\n *\n * @param event - Mouse event with final cell context\n * @returns `true` if drag was finalized, `false`/`void` otherwise\n *\n * @example\n * ```ts\n * onCellMouseUp(event: CellMouseEvent): boolean | void {\n * if (this.isDragging) {\n * this.finalizeDragSelection();\n * this.isDragging = false;\n * return true;\n * }\n * }\n * ```\n */\n onCellMouseUp?(event: CellMouseEvent): boolean | void;\n\n // Note: Context menu items are provided via handleQuery('getContextMenuItems').\n // This keeps the core decoupled from the context-menu plugin specifics.\n\n // #endregion\n\n // #region Column State Hooks\n\n /**\n * Contribute plugin-specific state for a column.\n * Called by the grid when collecting column state for serialization.\n * Plugins can add their own properties to the column state.\n *\n * @param field - The field name of the column\n * @returns Partial column state with plugin-specific properties, or undefined if no state to contribute\n *\n * @example\n * ```ts\n * getColumnState(field: string): Partial<ColumnState> | undefined {\n * const filterModel = this.filterModels.get(field);\n * if (filterModel) {\n * // Uses module augmentation to add filter property to ColumnState\n * return { filter: filterModel } as Partial<ColumnState>;\n * }\n * return undefined;\n * }\n * ```\n */\n getColumnState?(field: string): Partial<ColumnState> | undefined;\n\n /**\n * Apply plugin-specific state to a column.\n * Called by the grid when restoring column state from serialized data.\n * Plugins should restore their internal state based on the provided state.\n *\n * @param field - The field name of the column\n * @param state - The column state to apply (may contain plugin-specific properties)\n *\n * @example\n * ```ts\n * applyColumnState(field: string, state: ColumnState): void {\n * // Check for filter property added via module augmentation\n * const filter = (state as any).filter;\n * if (filter) {\n * this.filterModels.set(field, filter);\n * this.applyFilter();\n * }\n * }\n * ```\n */\n applyColumnState?(field: string, state: ColumnState): void;\n\n // #endregion\n\n // #region Scroll Boundary Hooks\n\n /**\n * Report horizontal scroll boundary offsets for this plugin.\n * Plugins that obscure part of the scroll area (e.g., pinned/sticky columns)\n * should return how much space they occupy on each side.\n * The keyboard navigation uses this to ensure focused cells are fully visible.\n *\n * @param rowEl - The row element (optional, for calculating widths from rendered cells)\n * @param focusedCell - The currently focused cell element (optional, to determine if scrolling should be skipped)\n * @returns Object with left/right pixel offsets and optional skipScroll flag, or undefined if plugin has no offsets\n *\n * @example\n * ```ts\n * getHorizontalScrollOffsets(rowEl?: HTMLElement, focusedCell?: HTMLElement): { left: number; right: number; skipScroll?: boolean } | undefined {\n * // Calculate total width of left-pinned columns\n * const leftCells = rowEl?.querySelectorAll('.sticky-left') ?? [];\n * let left = 0;\n * leftCells.forEach(el => { left += (el as HTMLElement).offsetWidth; });\n * // Skip scroll if focused cell is pinned (always visible)\n * const skipScroll = focusedCell?.classList.contains('sticky-left');\n * return { left, right: 0, skipScroll };\n * }\n * ```\n */\n getHorizontalScrollOffsets?(\n rowEl?: HTMLElement,\n focusedCell?: HTMLElement,\n ): { left: number; right: number; skipScroll?: boolean } | undefined;\n\n // #endregion\n\n // #region Shell Integration Hooks\n\n /**\n * Register a tool panel for this plugin.\n * Return undefined if plugin has no tool panel.\n * The shell will create a toolbar toggle button and render the panel content\n * when the user opens the panel.\n *\n * @returns Tool panel definition, or undefined if plugin has no panel\n *\n * @example\n * ```ts\n * getToolPanel(): ToolPanelDefinition | undefined {\n * return {\n * id: 'columns',\n * title: 'Columns',\n * icon: '☰',\n * tooltip: 'Show/hide columns',\n * order: 10,\n * render: (container) => {\n * this.renderColumnList(container);\n * return () => this.cleanup();\n * },\n * };\n * }\n * ```\n */\n getToolPanel?(): ToolPanelDefinition | undefined;\n\n /**\n * Register content for the shell header center section.\n * Return undefined if plugin has no header content.\n * Examples: search input, selection summary, status indicators.\n *\n * @returns Header content definition, or undefined if plugin has no header content\n *\n * @example\n * ```ts\n * getHeaderContent(): HeaderContentDefinition | undefined {\n * return {\n * id: 'quick-filter',\n * order: 10,\n * render: (container) => {\n * const input = document.createElement('input');\n * input.type = 'text';\n * input.placeholder = 'Search...';\n * input.addEventListener('input', this.handleInput);\n * container.appendChild(input);\n * return () => input.removeEventListener('input', this.handleInput);\n * },\n * };\n * }\n * ```\n */\n getHeaderContent?(): HeaderContentDefinition | undefined;\n\n // #endregion\n}\n","/**\n * 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// #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 // ─── Shared Attributes (used by plugins) ──────────────────────────\n GROUP_KEY: 'data-group-key', // GroupingRowsPlugin\n} as const;\n\n// #endregion\n\n// #region Selectors\n\n/**\n * Common CSS selectors for querying grid elements.\n * Built from the class constants for consistency.\n *\n * @category Plugin Development\n * @since 0.1.1\n */\nexport const GridSelectors = {\n ROOT: `.${GridClasses.ROOT}`,\n HEADER: `.${GridClasses.HEADER}`,\n HEADER_ROW: `.${GridClasses.HEADER_ROW}`,\n HEADER_CELL: `.${GridClasses.HEADER_CELL}`,\n ROWS_VIEWPORT: `.${GridClasses.ROWS_VIEWPORT}`,\n ROWS_CONTAINER: `.${GridClasses.ROWS_CONTAINER}`,\n DATA_ROW: `.${GridClasses.DATA_ROW}`,\n DATA_CELL: `.${GridClasses.DATA_CELL}`,\n GROUP_ROW: `.${GridClasses.GROUP_ROW}`,\n\n // By data attribute\n CELL_BY_FIELD: (field: string) => `.${GridClasses.DATA_CELL}[${GridDataAttrs.FIELD}=\"${field}\"]`,\n\n // State selectors\n SELECTED_ROWS: `.${GridClasses.DATA_ROW}.${GridClasses.SELECTED}`,\n EDITING_CELL: `.${GridClasses.DATA_CELL}.${GridClasses.EDITING}`,\n} as const;\n\n// #endregion\n\n// #region CSS Custom Properties\n\n/**\n * CSS custom property names for theming.\n * Use these when programmatically setting styles.\n *\n * @category Plugin Development\n * @since 0.1.1\n */\nexport const GridCSSVars = {\n // Colors\n COLOR_BG: '--tbw-color-bg',\n COLOR_FG: '--tbw-color-fg',\n COLOR_FG_MUTED: '--tbw-color-fg-muted',\n COLOR_BORDER: '--tbw-color-border',\n COLOR_ACCENT: '--tbw-color-accent',\n COLOR_HEADER_BG: '--tbw-color-header-bg',\n COLOR_HEADER_FG: '--tbw-color-header-fg',\n COLOR_SELECTION: '--tbw-color-selection',\n COLOR_ROW_HOVER: '--tbw-color-row-hover',\n COLOR_ROW_ALT: '--tbw-color-row-alt',\n\n // Sizing\n ROW_HEIGHT: '--tbw-row-height',\n HEADER_HEIGHT: '--tbw-header-height',\n CELL_PADDING: '--tbw-cell-padding',\n\n // Typography\n FONT_FAMILY: '--tbw-font-family',\n FONT_SIZE: '--tbw-font-size',\n\n // Borders\n BORDER_RADIUS: '--tbw-border-radius',\n FOCUS_OUTLINE: '--tbw-focus-outline',\n} as const;\n\n// #endregion\n\n// Type helpers\n\n/**\n * Union of all CSS class names used in the grid's DOM.\n * Use `GridClasses` to access individual values by key.\n *\n * @category Plugin Development\n * @since 0.1.1\n */\nexport type GridClassName = (typeof GridClasses)[keyof typeof GridClasses];\n\n/**\n * Union of all `data-*` attribute names used on grid elements.\n * Use `GridDataAttrs` to access individual values by key.\n *\n * @category Plugin Development\n * @since 0.1.1\n */\nexport type GridDataAttr = (typeof GridDataAttrs)[keyof typeof GridDataAttrs];\n\n/**\n * Union of all CSS custom property names (`--tbw-*`) available for theming.\n * Use `GridCSSVars` to access individual values by key.\n *\n * @category Plugin Development\n * @since 0.1.1\n */\nexport type GridCSSVar = (typeof GridCSSVars)[keyof typeof GridCSSVars];\n","/**\n * DOM Builder - Direct DOM construction for performance.\n *\n * Using direct DOM APIs instead of innerHTML is significantly faster:\n * - No HTML parsing by the browser\n * - No template string concatenation\n * - Immediate element creation without serialization/deserialization\n *\n * Benchmark: DOM construction is ~2-3x faster than innerHTML for complex structures.\n */\n\nimport { GridClasses } from '../constants';\nimport { sanitizeHTML } from './sanitize';\n\n// #region Element Factories\n/**\n * Apply attributes to an element, skipping null/undefined values.\n * Shared by createElement/div/button to keep the bundle small.\n */\nfunction applyAttrs(el: Element, attrs: Record<string, string | undefined> | undefined): void {\n if (!attrs) return;\n for (const key in attrs) {\n const value = attrs[key];\n if (value != null) el.setAttribute(key, value);\n }\n}\n\n/**\n * Create an element with attributes and optional children.\n * Optimized helper that avoids repeated function calls.\n */\nfunction createElement<K extends keyof HTMLElementTagNameMap>(\n tag: K,\n attrs?: Record<string, string>,\n children?: (Node | string | null | undefined)[],\n): HTMLElementTagNameMap[K] {\n const el = document.createElement(tag);\n applyAttrs(el, attrs);\n\n if (children) {\n for (const child of children) {\n if (child == null) continue;\n if (typeof child === 'string') {\n el.appendChild(document.createTextNode(child));\n } else {\n el.appendChild(child);\n }\n }\n }\n\n return el;\n}\n\n/**\n * Create an element with class (common pattern).\n */\nfunction div(className?: string, attrs?: Record<string, string>): HTMLDivElement {\n const el = document.createElement('div');\n if (className) el.className = className;\n applyAttrs(el, attrs);\n return el;\n}\n\n/**\n * Create a button element. Always sets `type=\"button\"` (callers can override\n * via `attrs`) so internal grid buttons inside a <form> never default to\n * type=\"submit\". See issue #296.\n */\nfunction button(className?: string, attrs?: Record<string, string>, content?: string | Node): HTMLButtonElement {\n const el = document.createElement('button');\n el.type = 'button';\n if (className) el.className = className;\n applyAttrs(el, attrs);\n if (content) {\n if (typeof content === 'string') {\n el.textContent = content;\n } else {\n el.appendChild(content);\n }\n }\n return el;\n}\n// #endregion\n\n// #region Grid Templates\n/**\n * Template for grid content (the core scrollable grid area).\n * Pre-built once, then cloned for each grid instance.\n */\nconst gridContentTemplate = document.createElement('template');\ngridContentTemplate.innerHTML = `\n <div class=\"tbw-scroll-area\">\n <div class=\"rows-body-wrapper\">\n <div class=\"rows-body\" role=\"grid\">\n <div class=\"header\" role=\"rowgroup\">\n <div class=\"header-row\" role=\"row\" part=\"header-row\"></div>\n </div>\n <div class=\"rows-container\" role=\"presentation\">\n <div class=\"rows-viewport\" role=\"presentation\">\n <div class=\"rows\"></div>\n </div>\n </div>\n </div>\n </div>\n </div>\n <div class=\"faux-vscroll\">\n <div class=\"faux-vscroll-spacer\"></div>\n </div>\n <div class=\"tbw-sr-only\" aria-live=\"polite\" aria-atomic=\"true\"></div>\n`;\n\n/**\n * Clone the grid content structure.\n * Using template cloning is faster than createElement for nested structures.\n */\nexport function cloneGridContent(): DocumentFragment {\n return gridContentTemplate.content.cloneNode(true) as DocumentFragment;\n}\n// #endregion\n\n// #region Grid DOM Building\n/**\n * Build the grid root structure using direct DOM construction.\n * This is called once per grid instance during initial render.\n */\nexport interface GridDOMOptions {\n hasShell: boolean;\n /** Shell header element (pre-built) */\n shellHeader?: Element;\n /** Shell body element with tool panel (pre-built) */\n shellBody?: Element;\n}\n\n/**\n * Build the complete grid DOM structure.\n * Returns a DocumentFragment ready to be appended to shadow root.\n */\nexport function buildGridDOM(options: GridDOMOptions): DocumentFragment {\n const fragment = document.createDocumentFragment();\n\n const root = div(options.hasShell ? `${GridClasses.ROOT} has-shell` : GridClasses.ROOT);\n\n if (options.hasShell && options.shellBody) {\n // Shell mode: optional header + body (with grid content inside).\n // The header bar may be suppressed via `shell.header.visible: false`, in\n // which case only the body (and its tool panel) is rendered.\n if (options.shellHeader) root.appendChild(options.shellHeader);\n root.appendChild(options.shellBody);\n } else {\n // No shell: just grid content in wrapper\n const contentWrapper = div('tbw-grid-content');\n contentWrapper.appendChild(cloneGridContent());\n root.appendChild(contentWrapper);\n }\n\n fragment.appendChild(root);\n return fragment;\n}\n\n/**\n * Build the BARE grid DOM (no shell chrome) directly into `renderRoot`.\n *\n * Preserves all existing direct child nodes EXCEPT the grid's own\n * `.tbw-grid-root` (which is rebuilt), so user/framework light-DOM stays intact\n * in its original order — including non-element nodes (comment anchors used by\n * Angular `@if`/`@for` and other structural directives as positional markers).\n *\n * Core's render path always builds the bare grid; the shell, when configured,\n * is wrapped around this freshly built grid by the auto-registered ShellPlugin\n * in the `afterStructuralRender` hook (extraction #370 — DOM ownership\n * inversion). Core therefore holds no shell-construction knowledge.\n */\nexport function buildBareGridDOMIntoElement(renderRoot: Element): void {\n const preservedNodes: Node[] = [];\n for (let n: Node | null = renderRoot.firstChild; n; n = n.nextSibling) {\n if (n.nodeType === 1 && (n as Element).classList.contains(GridClasses.ROOT)) continue;\n preservedNodes.push(n);\n }\n\n // Clear existing content (this would delete light DOM nodes, so we preserved them first).\n renderRoot.replaceChildren();\n\n // Re-append preserved nodes in original order (hidden elements are used for config).\n // IMPORTANT: These are prepended before .tbw-grid-root, so `renderRoot.children[0]`\n // is NOT the grid root. Use `querySelector('.tbw-grid-root')` instead.\n for (const n of preservedNodes) {\n renderRoot.appendChild(n);\n }\n\n renderRoot.appendChild(buildGridDOM({ hasShell: false }));\n}\n// #endregion\n\n// #region Shell Header\n/**\n * Build shell header using direct DOM construction.\n *\n * Note: The grid no longer creates buttons from config (icon/action).\n * Config/API buttons only use element or render function.\n * Light DOM buttons are slotted directly.\n * The ONLY button the grid creates is the panel toggle.\n */\nexport interface ShellHeaderOptions {\n title?: string;\n hasPanels: boolean;\n isPanelOpen: boolean;\n toolPanelIcon?: string;\n /**\n * Whether to render the built-in tool panel toggle button and the\n * adjacent separator. Independent of `hasPanels` so panels can remain\n * registered (and openable via the public API) while the consumer\n * supplies their own toggle button.\n * @default true when hasPanels is true\n */\n showToggle?: boolean;\n /** Config toolbar contents with render function (pre-sorted by order) */\n configButtons: Array<{\n id: string;\n hasRender?: boolean;\n }>;\n /** API toolbar contents with render function (pre-sorted by order) */\n apiButtons: Array<{\n id: string;\n hasRender?: boolean;\n }>;\n}\n\n/**\n * Build shell header element directly without innerHTML.\n */\nexport function buildShellHeader(options: ShellHeaderOptions): HTMLDivElement {\n const header = div('tbw-shell-header', { part: 'shell-header', role: 'presentation' });\n\n // Title\n if (options.title) {\n const titleEl = div('tbw-shell-title');\n titleEl.textContent = options.title;\n header.appendChild(titleEl);\n }\n\n // Shell content with placeholder for light DOM header content\n const content = div('tbw-shell-content', {\n part: 'shell-content',\n role: 'presentation',\n 'data-light-dom-header-content': '',\n });\n header.appendChild(content);\n\n // Toolbar\n const toolbar = div('tbw-shell-toolbar', { part: 'shell-toolbar', role: 'presentation' });\n\n // Placeholders for config toolbar contents with render function\n for (const btn of options.configButtons) {\n if (btn.hasRender) {\n toolbar.appendChild(div('tbw-toolbar-content-slot', { 'data-toolbar-content': btn.id }));\n }\n }\n // Placeholders for API toolbar contents with render function\n for (const btn of options.apiButtons) {\n if (btn.hasRender) {\n toolbar.appendChild(div('tbw-toolbar-content-slot', { 'data-toolbar-content': btn.id }));\n }\n }\n\n // Separator between custom content and panel toggle\n const hasCustomContent =\n options.configButtons.some((b) => b.hasRender) || options.apiButtons.some((b) => b.hasRender);\n // Default showToggle to hasPanels for backwards compat; suppress both the\n // button and the separator when the consumer opts out.\n const showToggle = options.hasPanels && options.showToggle !== false;\n if (hasCustomContent && showToggle) {\n toolbar.appendChild(div('tbw-toolbar-separator'));\n }\n\n // Panel toggle button (suppressed when shell.header.toolPanelToggle is false)\n if (showToggle) {\n const toggleBtn = button(options.isPanelOpen ? 'tbw-toolbar-btn active' : 'tbw-toolbar-btn', {\n 'data-panel-toggle': '',\n title: 'Settings',\n 'aria-label': 'Toggle settings panel',\n 'aria-pressed': String(options.isPanelOpen),\n 'aria-controls': 'tbw-tool-panel',\n });\n toggleBtn.dataset.icon = 'tool-panel';\n if (options.toolPanelIcon !== undefined) {\n toggleBtn.innerHTML = sanitizeHTML(options.toolPanelIcon);\n }\n toolbar.appendChild(toggleBtn);\n }\n\n header.appendChild(toolbar);\n return header;\n}\n// #endregion\n\n// #region Shell Body\n/**\n * Build shell body (grid content + optional tool panel).\n */\nexport interface ShellBodyOptions {\n position: 'left' | 'right';\n /**\n * Layout mode for the tool panel.\n * - `'overlay'` (default): panel is absolutely positioned over the grid.\n * - `'push'`: panel participates in flex layout, shrinking the grid sibling.\n * - `'dropdown'`: panel is a native popover anchored near a trigger element.\n */\n mode?: 'overlay' | 'push' | 'dropdown';\n isPanelOpen: boolean;\n expandIcon?: string;\n collapseIcon?: string;\n /**\n * Render an in-panel close (✕) button in the panel's top-right corner.\n * Used when the shell header bar is hidden (`shell.header.visible: false`)\n * so the panel can be dismissed without the built-in header toggle.\n */\n showCloseButton?: boolean;\n /** Sorted panels for accordion */\n panels: Array<{\n id: string;\n title: string;\n icon?: string;\n isExpanded: boolean;\n }>;\n}\n\n/**\n * Build shell body element directly without innerHTML.\n */\nexport function buildShellBody(options: ShellBodyOptions): HTMLDivElement {\n const mode = options.mode ?? 'overlay';\n const body = div('tbw-shell-body', { 'data-mode': mode });\n const hasPanel = options.panels.length > 0;\n const isSinglePanel = options.panels.length === 1;\n\n // Grid content wrapper with cloned grid structure\n const gridContent = div('tbw-grid-content');\n gridContent.appendChild(cloneGridContent());\n\n // Tool panel\n let panelEl: HTMLElement | null = null;\n if (hasPanel) {\n panelEl = createElement('aside', {\n class: options.isPanelOpen ? 'tbw-tool-panel open' : 'tbw-tool-panel',\n part: 'tool-panel',\n 'data-position': options.position,\n role: 'presentation',\n id: 'tbw-tool-panel',\n // In dropdown mode the panel is a manually-controlled native popover\n // (shown/hidden via showPopover()/hidePopover() from the controller).\n ...(mode === 'dropdown' ? { popover: 'manual' } : {}),\n });\n\n // Resize handle\n const resizeHandlePosition = options.position === 'left' ? 'right' : 'left';\n panelEl.appendChild(\n div('tbw-tool-panel-resize', {\n 'data-resize-handle': '',\n 'data-handle-position': resizeHandlePosition,\n 'aria-hidden': 'true',\n }),\n );\n\n // Optional close (✕) button — rendered (overlay/push only) when the shell\n // header bar is hidden, so the panel can always be dismissed. Dropdown mode\n // never reaches here (showCloseButton is gated off upstream). `data-panel-close`\n // is wired by setupShellEventListeners to close the panel.\n //\n // Placement:\n // - Multiple panels: a dedicated slim header row above the accordion.\n // - Single panel: inlined onto the (only) accordion header row so it shares\n // the line with the panel title instead of wasting a full-width row.\n const makeCloseButton = (inline: boolean): HTMLButtonElement => {\n const closeBtn = button(inline ? 'tbw-tool-panel-close tbw-tool-panel-close--inline' : 'tbw-tool-panel-close', {\n part: 'tool-panel-close',\n 'data-panel-close': '',\n title: 'Close panel',\n 'aria-label': 'Close panel',\n 'aria-controls': 'tbw-tool-panel',\n });\n closeBtn.textContent = '✕';\n return closeBtn;\n };\n const inlineClose = options.showCloseButton === true && isSinglePanel;\n if (options.showCloseButton && !isSinglePanel) {\n const panelHeader = div('tbw-tool-panel-header', { part: 'tool-panel-header' });\n panelHeader.appendChild(makeCloseButton(false));\n panelEl.appendChild(panelHeader);\n }\n\n // Panel content with accordion\n const panelContent = div('tbw-tool-panel-content', { role: 'presentation' });\n const accordion = div('tbw-accordion');\n\n for (const panel of options.panels) {\n const sectionClasses = `tbw-accordion-section${panel.isExpanded ? ' expanded' : ''}${isSinglePanel ? ' single' : ''}${inlineClose ? ' has-inline-close' : ''}`;\n const section = div(sectionClasses, { 'data-section': panel.id });\n\n // Accordion header button\n const headerBtn = button('tbw-accordion-header', {\n 'aria-expanded': String(panel.isExpanded),\n 'aria-controls': `tbw-section-${panel.id}`,\n });\n if (isSinglePanel) headerBtn.setAttribute('aria-disabled', 'true');\n\n // Icon\n if (panel.icon) {\n const iconSpan = createElement('span', { class: 'tbw-accordion-icon' });\n iconSpan.innerHTML = sanitizeHTML(panel.icon);\n headerBtn.appendChild(iconSpan);\n }\n\n // Title\n const titleSpan = createElement('span', { class: 'tbw-accordion-title' });\n titleSpan.textContent = panel.title;\n headerBtn.appendChild(titleSpan);\n\n // Chevron (hidden for single panel) — always use expandIcon, CSS rotation handles state\n if (!isSinglePanel) {\n const chevronSpan = createElement('span', { class: 'tbw-accordion-chevron', 'data-icon': 'expand' });\n if (options.expandIcon !== undefined) {\n chevronSpan.innerHTML = sanitizeHTML(options.expandIcon);\n }\n headerBtn.appendChild(chevronSpan);\n }\n\n // Single-panel close button shares the header row with the panel title.\n if (inlineClose) {\n const headerRow = div('tbw-accordion-header-row');\n headerRow.appendChild(headerBtn);\n headerRow.appendChild(makeCloseButton(true));\n section.appendChild(headerRow);\n } else {\n section.appendChild(headerBtn);\n }\n\n // Accordion content (empty, will be filled by panel render functions)\n section.appendChild(\n div('tbw-accordion-content', {\n id: `tbw-section-${panel.id}`,\n role: 'presentation',\n }),\n );\n\n accordion.appendChild(section);\n }\n\n panelContent.appendChild(accordion);\n panelEl.appendChild(panelContent);\n }\n\n // Append in correct order based on position\n if (options.position === 'left' && panelEl) {\n body.appendChild(panelEl);\n body.appendChild(gridContent);\n } else {\n body.appendChild(gridContent);\n if (panelEl) body.appendChild(panelEl);\n }\n\n return body;\n}\n// #endregion\n","/**\n * Shell infrastructure for grid header bar and tool panels.\n *\n * The shell is an optional wrapper layer that provides:\n * - Header bar with title, plugin content, and toolbar buttons\n * - Tool panels that plugins can register content into\n * - Light DOM parsing for framework-friendly configuration\n */\n\nimport { TOOL_PANEL_DUPLICATE, TOOL_PANEL_MISSING_ATTR, warnDiagnostic } from '../../core/internal/diagnostics';\nimport { escapeHtml, sanitizeHTML } from '../../core/internal/sanitize';\nimport type { IconValue } from '../../core/types';\nimport type { HeaderContentDefinition, ShellConfig, ToolbarContentDefinition, ToolPanelDefinition } from './types';\n\n// #region Types & State\n/**\n * Convert an IconValue to a string for rendering in HTML.\n */\nfunction iconToString(icon: IconValue | undefined): string {\n if (!icon) return '';\n if (typeof icon === 'string') return sanitizeHTML(icon);\n // For HTMLElement, get the outerHTML\n return icon.outerHTML;\n}\n\n/**\n * State for managing shell UI.\n *\n * This interface holds both configuration-like properties (toolPanels, headerContents)\n * and runtime state (isPanelOpen, expandedSections). The Maps allow for efficient\n * registration/unregistration of panels and content.\n */\nexport interface ShellState {\n /** Registered tool panels (from plugins + consumer API) */\n toolPanels: Map<string, ToolPanelDefinition>;\n /** Registered header content (from plugins + consumer API) */\n headerContents: Map<string, HeaderContentDefinition>;\n /** Toolbar content registered via API or light DOM */\n toolbarContents: Map<string, ToolbarContentDefinition>;\n /** Whether a <tbw-grid-tool-buttons> container was found in light DOM */\n hasToolButtonsContainer: boolean;\n /** Light DOM header content elements */\n lightDomHeaderContent: HTMLElement[];\n /** Light DOM header title from <tbw-grid-header title=\"...\"> */\n lightDomTitle: string | null;\n /** IDs of tool panels registered from light DOM (to avoid re-parsing) */\n lightDomToolPanelIds: Set<string>;\n /**\n * IDs of light DOM tool panels whose `render` has already been bound to a\n * framework-adapter renderer. Used by `parseLightDomToolPanels` to detect\n * the FIRST adapter attach (which legitimately needs to swap the vanilla\n * fallback for the real adapter renderer and tear down any in-progress\n * render) vs. subsequent re-parses (which must NOT tear down — doing so\n * unmounts the user's React/Vue/Angular component, losing local state and\n * resetting scroll position inside the panel).\n */\n adapterBoundToolPanelIds: Set<string>;\n /** IDs of toolbar content registered from light DOM (to avoid re-parsing) */\n lightDomToolbarContentIds: Set<string>;\n /** IDs of tool panels registered via registerToolPanel API */\n apiToolPanelIds: Set<string>;\n /** IDs of header content registered via registerHeaderContent API */\n apiHeaderContentIds: Set<string>;\n /** Whether the tool panel sidebar is open */\n isPanelOpen: boolean;\n /** Which accordion sections are expanded (by panel ID) */\n expandedSections: Set<string>;\n /** Whether light DOM header content has been moved to placeholder (perf optimization) */\n lightDomContentMoved: boolean;\n /** Cleanup functions for header content render returns */\n headerContentCleanups: Map<string, () => void>;\n /** Cleanup functions for each panel section's render return */\n panelCleanups: Map<string, () => void>;\n /** Cleanup functions for toolbar content render returns */\n toolbarContentCleanups: Map<string, () => void>;\n /**\n * Element the dropdown popover is currently anchored to (mode: 'dropdown').\n * Tracked so dismissal logic can ignore clicks on the anchor and the\n * `anchor-name` it set can be cleared on close. `null` when no dropdown is open.\n */\n dropdownAnchorEl?: HTMLElement | null;\n /**\n * Unique `anchor-name` assigned to the current dropdown's trigger (and\n * mirrored as `position-anchor` on the popover). Minted per show so multiple\n * grids on one page never collide on a shared anchor name — a collision would\n * position one grid's popover against another grid's trigger. `null` when no\n * dropdown is open.\n */\n dropdownAnchorName?: string | null;\n}\n\n/**\n * Runtime-only shell state (not configuration).\n *\n * Configuration (toolPanels, headerContents, toolbarContents, title) lives in\n * effectiveConfig.shell. This state holds runtime UI state and cleanup functions.\n */\nexport interface ShellRuntimeState {\n /** Whether the tool panel sidebar is currently open */\n isPanelOpen: boolean;\n /** Which accordion sections are currently expanded (by panel ID) */\n expandedSections: Set<string>;\n /** Cleanup functions for header content render returns */\n headerContentCleanups: Map<string, () => void>;\n /** Cleanup functions for each panel section's render return */\n panelCleanups: Map<string, () => void>;\n /** Cleanup functions for toolbar content render returns */\n toolbarContentCleanups: Map<string, () => void>;\n /** IDs of tool panels registered from light DOM (to avoid re-parsing) */\n lightDomToolPanelIds: Set<string>;\n /** IDs of tool panels registered via registerToolPanel API */\n apiToolPanelIds: Set<string>;\n /** Whether a <tbw-grid-tool-buttons> container was found in light DOM */\n hasToolButtonsContainer: boolean;\n}\n\n/**\n * Create initial shell runtime state.\n */\nexport function createShellRuntimeState(): ShellRuntimeState {\n return {\n isPanelOpen: false,\n expandedSections: new Set(),\n headerContentCleanups: new Map(),\n panelCleanups: new Map(),\n toolbarContentCleanups: new Map(),\n lightDomToolPanelIds: new Set(),\n apiToolPanelIds: new Set(),\n hasToolButtonsContainer: false,\n };\n}\n\n/**\n * Create initial shell state.\n */\nexport function createShellState(): ShellState {\n return {\n toolPanels: new Map(),\n headerContents: new Map(),\n toolbarContents: new Map(),\n hasToolButtonsContainer: false,\n lightDomHeaderContent: [],\n lightDomTitle: null,\n lightDomToolPanelIds: new Set(),\n adapterBoundToolPanelIds: new Set(),\n lightDomToolbarContentIds: new Set(),\n apiToolPanelIds: new Set(),\n apiHeaderContentIds: new Set(),\n isPanelOpen: false,\n expandedSections: new Set(),\n headerContentCleanups: new Map(),\n panelCleanups: new Map(),\n toolbarContentCleanups: new Map(),\n lightDomContentMoved: false,\n dropdownAnchorEl: null,\n dropdownAnchorName: null,\n };\n}\n// #endregion\n\n// #region Render Functions\n/**\n * Determine if shell header should be rendered.\n * Reads only from effectiveConfig.shell (single source of truth).\n */\nexport function shouldRenderShellHeader(config: ShellConfig | undefined): boolean {\n // Check if title is configured\n if (config?.header?.title) return true;\n\n // Check if config has toolbar contents\n if (config?.header?.toolbarContents?.length) return true;\n\n // Check if any tool panels are registered\n if (config?.toolPanels?.length) return true;\n\n // Check if any header content is registered\n if (config?.headerContents?.length) return true;\n\n // Check if light DOM has header elements\n if (config?.header?.lightDomContent?.length) return true;\n\n // Check if a toolbar buttons container was found\n if (config?.header?.hasToolButtonsContainer) return true;\n\n return false;\n}\n\n/**\n * Determine whether the visible shell header bar (`.tbw-shell-header`) should\n * be rendered.\n *\n * Distinct from {@link shouldRenderShellHeader}, which decides whether the\n * shell *wrapper* (including the body that hosts the tool panel) exists at\n * all. The header bar can be suppressed via `header.visible: false` while the\n * shell body and tool panels still render — letting consumers drive panels\n * from their own UI (e.g. a utility-column header icon).\n */\nexport function shouldRenderHeaderBar(config: ShellConfig | undefined): boolean {\n return config?.header?.visible !== false;\n}\n\n/**\n * Render the shell header HTML.\n *\n * Toolbar contents come from two sources:\n * 1. Light DOM slot (users provide their own HTML in <tbw-grid-tool-buttons>)\n * 2. Config/API with render function (programmatic insertion)\n *\n * Users have full control over toolbar HTML, styling, and behavior.\n * The only button the grid creates is the tool panel toggle.\n *\n * @param toolPanelIcon - Icon for the tool panel toggle (from grid icon config)\n */\nexport function renderShellHeader(\n config: ShellConfig | undefined,\n state: ShellState,\n toolPanelIcon?: IconValue,\n): string {\n const title = config?.header?.title ?? state.lightDomTitle ?? '';\n const hasTitle = !!title;\n\n // Build tool panel button content: use data-icon for CSS, inject content only for JS overrides\n let toolPanelBtnContent = '';\n if (toolPanelIcon !== undefined) {\n // JS override: inject content but still set data-icon\n toolPanelBtnContent = typeof toolPanelIcon === 'string' ? sanitizeHTML(toolPanelIcon) : toolPanelIcon.outerHTML;\n }\n\n // Get all toolbar contents from effectiveConfig (already merged: config + API + light DOM)\n // The config-manager merges state.toolbarContents into effectiveConfig.shell.header.toolbarContents\n // Also include state.toolbarContents directly for cases where renderShellHeader is called\n // before config-manager has merged (e.g., unit tests, initial render)\n const configContents = config?.header?.toolbarContents ?? [];\n const stateContents = [...state.toolbarContents.values()];\n\n // Merge: use config contents, add state contents that aren't in config\n const configIds = new Set(configContents.map((c) => c.id));\n const allContents = [...configContents];\n for (const content of stateContents) {\n if (!configIds.has(content.id)) {\n allContents.push(content);\n }\n }\n\n const hasCustomContent = allContents.length > 0;\n const hasPanels = state.toolPanels.size > 0;\n // Allow consumers to suppress the built-in toggle (and the separator that\n // only exists to space it from custom toolbar contents) so they can render\n // their own design-system button and wire it to `grid.toggleToolPanel()`.\n // Also suppress when the tool panel is locked open — toggling is disabled.\n // Default true preserves existing behavior.\n const isLocked = config?.toolPanel?.locked === true;\n const showToggle = hasPanels && config?.header?.toolPanelToggle !== false && !isLocked;\n const showSeparator = hasCustomContent && showToggle;\n\n // Sort contents by order for slot placement\n const sortedContents = [...allContents].sort((a, b) => (a.order ?? 0) - (b.order ?? 0));\n\n // Build toolbar HTML\n let toolbarHtml = '';\n\n // Create slots for all contents (unified: config + API + light DOM)\n for (const content of sortedContents) {\n toolbarHtml += `<div class=\"tbw-toolbar-content-slot\" data-toolbar-content=\"${content.id}\"></div>`;\n }\n\n // Separator between custom content and panel toggle\n if (showSeparator) {\n toolbarHtml += '<div class=\"tbw-toolbar-separator\"></div>';\n }\n\n // Single panel toggle button (the ONLY button the grid creates).\n // Suppressed when `shell.header.toolPanelToggle === false` so consumers can\n // BYO toggle button without hiding library DOM via CSS.\n if (showToggle) {\n const isOpen = state.isPanelOpen;\n const toggleClass = isOpen ? 'tbw-toolbar-btn active' : 'tbw-toolbar-btn';\n // type=\"button\" is required: without it a <button> inside a <form>\n // defaults to type=\"submit\" and clicking the toggle would submit the\n // surrounding form. Internal grid buttons must never trigger form\n // submission. See issue #296.\n toolbarHtml += `<button type=\"button\" class=\"${toggleClass}\" data-panel-toggle data-icon=\"tool-panel\" title=\"Settings\" aria-label=\"Toggle settings panel\" aria-pressed=\"${isOpen}\" aria-controls=\"tbw-tool-panel\">${toolPanelBtnContent}</button>`;\n }\n\n return `\n <div class=\"tbw-shell-header\" part=\"shell-header\" role=\"presentation\">\n ${hasTitle ? `<div class=\"tbw-shell-title\">${escapeHtml(title)}</div>` : ''}\n <div class=\"tbw-shell-content\" part=\"shell-content\" role=\"presentation\" data-light-dom-header-content></div>\n <div class=\"tbw-shell-toolbar\" part=\"shell-toolbar\" role=\"presentation\">\n ${toolbarHtml}\n </div>\n </div>\n `;\n}\n\n/**\n * Render the shell body wrapper HTML (contains grid content + accordion-style tool panel).\n * @param icons - Optional icons for expand/collapse chevrons (from grid config)\n */\nexport function renderShellBody(\n config: ShellConfig | undefined,\n state: ShellState,\n gridContentHtml: string,\n icons?: { expand?: IconValue; collapse?: IconValue },\n): string {\n const position = config?.toolPanel?.position ?? 'right';\n const mode = config?.toolPanel?.mode ?? 'overlay';\n const hasPanel = state.toolPanels.size > 0;\n const isOpen = state.isPanelOpen;\n const hasJsExpandIcon = icons?.expand !== undefined;\n const expandIcon = hasJsExpandIcon ? iconToString(icons.expand) : '';\n\n // Sort panels by order for accordion sections\n const sortedPanels = [...state.toolPanels.values()].sort((a, b) => (a.order ?? 100) - (b.order ?? 100));\n const isSinglePanel = sortedPanels.length === 1;\n\n // Build accordion sections HTML\n let accordionHtml = '';\n for (const panel of sortedPanels) {\n const isExpanded = state.expandedSections.has(panel.id);\n const iconHtml = panel.icon ? `<span class=\"tbw-accordion-icon\">${panel.icon}</span>` : '';\n // Hide chevron for single panel (no toggling needed)\n // Always use expandIcon (▶) — CSS rotation handles the expanded state\n const chevronHtml = isSinglePanel\n ? ''\n : `<span class=\"tbw-accordion-chevron\" data-icon=\"expand\">${expandIcon}</span>`;\n // Disable accordion toggle for single panel\n const sectionClasses = `tbw-accordion-section${isExpanded ? ' expanded' : ''}${isSinglePanel ? ' single' : ''}`;\n accordionHtml += `\n <div class=\"${sectionClasses}\" data-section=\"${panel.id}\">\n <button type=\"button\" class=\"tbw-accordion-header\" aria-expanded=\"${isExpanded}\" aria-controls=\"tbw-section-${panel.id}\"${isSinglePanel ? ' aria-disabled=\"true\"' : ''}>\n ${iconHtml}\n <span class=\"tbw-accordion-title\">${panel.title}</span>\n ${chevronHtml}\n </button>\n <div class=\"tbw-accordion-content\" id=\"tbw-section-${panel.id}\" role=\"presentation\"></div>\n </div>\n `;\n }\n\n // Resize handle position depends on panel position\n const resizeHandlePosition = position === 'left' ? 'right' : 'left';\n\n const panelHtml = hasPanel\n ? `\n <aside class=\"tbw-tool-panel${isOpen ? ' open' : ''}\" part=\"tool-panel\" data-position=\"${position}\" role=\"presentation\" id=\"tbw-tool-panel\"${mode === 'dropdown' ? ' popover=\"manual\"' : ''}>\n <div class=\"tbw-tool-panel-resize\" data-resize-handle data-handle-position=\"${resizeHandlePosition}\" aria-hidden=\"true\"></div>\n <div class=\"tbw-tool-panel-content\" role=\"presentation\">\n <div class=\"tbw-accordion\">\n ${accordionHtml}\n </div>\n </div>\n </aside>\n `\n : '';\n\n // For left position, panel comes before content in DOM order\n if (position === 'left') {\n return `\n <div class=\"tbw-shell-body\" data-mode=\"${mode}\">\n ${panelHtml}\n <div class=\"tbw-grid-content\">\n ${gridContentHtml}\n </div>\n </div>\n `;\n }\n\n return `\n <div class=\"tbw-shell-body\" data-mode=\"${mode}\">\n <div class=\"tbw-grid-content\">\n ${gridContentHtml}\n </div>\n ${panelHtml}\n </div>\n `;\n}\n// #endregion\n\n// #region Light DOM Parsing\n/**\n * Parse light DOM shell elements (tbw-grid-header, etc.).\n * Safe to call multiple times - will only parse once when elements are available.\n */\nexport function parseLightDomShell(host: HTMLElement, state: ShellState): void {\n const headerEl = host.querySelector('tbw-grid-header');\n if (!headerEl) return;\n\n // Parse title attribute (only if not already parsed)\n if (!state.lightDomTitle) {\n const title = headerEl.getAttribute('title');\n if (title) {\n state.lightDomTitle = title;\n }\n }\n\n // Parse header content elements - store references but don't set slot (light DOM doesn't use slots)\n const headerContents = headerEl.querySelectorAll('tbw-grid-header-content');\n if (headerContents.length > 0 && state.lightDomHeaderContent.length === 0) {\n state.lightDomHeaderContent = Array.from(headerContents) as HTMLElement[];\n }\n\n // Hide the light DOM header container (it was just for declarative config)\n (headerEl as HTMLElement).style.display = 'none';\n}\n\n/**\n * Callback type for creating a toolbar content renderer from a light DOM container.\n * This is used by framework adapters (Angular, React, etc.) to create renderers\n * from their template syntax.\n */\nexport type ToolbarContentRendererFactory = (\n container: HTMLElement,\n) => ((target: HTMLElement) => void | (() => void)) | undefined;\n\n/**\n * Parse toolbar buttons container element (<tbw-grid-tool-buttons>).\n * This is a content container - we don't parse individual children.\n * The entire container content is registered as a single toolbar content entry.\n *\n * Example:\n * ```html\n * <tbw-grid>\n * <tbw-grid-tool-buttons>\n * <button>My button</button>\n * <button>My other button</button>\n * </tbw-grid-tool-buttons>\n * </tbw-grid>\n * ```\n *\n * The container's children are moved to the toolbar area during render.\n * We treat this as opaque content - users control what goes inside.\n *\n * @param host - The grid host element\n * @param state - Shell state to update\n * @param rendererFactory - Optional factory for creating renderers (used by framework adapters)\n */\nexport function parseLightDomToolButtons(\n host: HTMLElement,\n state: ShellState,\n rendererFactory?: ToolbarContentRendererFactory,\n): void {\n // Look for the toolbar buttons container element\n const toolButtonsContainer = host.querySelector(':scope > tbw-grid-tool-buttons') as HTMLElement | null;\n if (!toolButtonsContainer) return;\n\n // Mark that we found the container (for shouldRenderShellHeader)\n state.hasToolButtonsContainer = true;\n\n // Skip if already registered\n const id = 'light-dom-toolbar-content';\n if (state.lightDomToolbarContentIds.has(id)) return;\n\n // Register as a single content entry with a render function\n const adapterRenderer = rendererFactory?.(toolButtonsContainer);\n\n const contentDef: ToolbarContentDefinition = {\n id,\n order: 0, // Light DOM content comes first\n render:\n adapterRenderer ??\n ((target: HTMLElement) => {\n // Move all children from the light DOM container to the target\n while (toolButtonsContainer.firstChild) {\n target.appendChild(toolButtonsContainer.firstChild);\n }\n // Return cleanup that moves children back to original container\n // This preserves them across full re-renders that destroy the slot\n return () => {\n while (target.firstChild) {\n toolButtonsContainer.appendChild(target.firstChild);\n }\n };\n }),\n };\n\n state.toolbarContents.set(id, contentDef);\n state.lightDomToolbarContentIds.add(id);\n\n // Hide the original container\n toolButtonsContainer.style.display = 'none';\n}\n\n/**\n * Callback type for creating a tool panel renderer from a light DOM element.\n * This is used by framework adapters (Angular, React, etc.) to create renderers\n * from their template syntax.\n */\nexport type ToolPanelRendererFactory = (\n element: HTMLElement,\n) => ((container: HTMLElement) => void | (() => void)) | undefined;\n\n/**\n * Parse light DOM tool panel elements (<tbw-grid-tool-panel>).\n * These can appear as direct children of <tbw-grid> for declarative tool panel configuration.\n *\n * Attributes:\n * - `id` (required): Unique panel identifier\n * - `title` (required): Panel title shown in accordion header\n * - `icon`: Icon for accordion section header (emoji or text)\n * - `tooltip`: Tooltip for accordion section header\n * - `order`: Panel order priority (lower = first, default: 100)\n *\n * For vanilla JS, the element's innerHTML is used as the panel content.\n * For framework adapters, the adapter can provide a custom renderer factory.\n *\n * @param host - The grid host element\n * @param state - Shell state to update\n * @param rendererFactory - Optional factory for creating renderers (used by framework adapters)\n */\nexport function parseLightDomToolPanels(\n host: HTMLElement,\n state: ShellState,\n rendererFactory?: ToolPanelRendererFactory,\n): void {\n const toolPanelElements = host.querySelectorAll(':scope > tbw-grid-tool-panel');\n\n toolPanelElements.forEach((element) => {\n const panelEl = element as HTMLElement;\n const id = panelEl.getAttribute('id');\n const title = panelEl.getAttribute('title');\n\n // Skip if required attributes are missing\n if (!id || !title) {\n warnDiagnostic(\n TOOL_PANEL_MISSING_ATTR,\n `Tool panel missing required id or title attribute: id=\"${id ?? ''}\", title=\"${title ?? ''}\"`,\n );\n return;\n }\n\n const icon = panelEl.getAttribute('icon') ?? undefined;\n const tooltip = panelEl.getAttribute('tooltip') ?? undefined;\n const order = parseInt(panelEl.getAttribute('order') ?? '100', 10);\n\n // Try framework adapter first, then fall back to innerHTML\n let render: (container: HTMLElement) => void | (() => void);\n\n const adapterRenderer = rendererFactory?.(panelEl);\n if (adapterRenderer) {\n render = adapterRenderer;\n } else {\n // Vanilla fallback: use sanitized innerHTML as static content.\n // Light DOM authored markup is generally trusted, but we sanitize\n // defensively in case the panel content was server-rendered from data.\n const content = sanitizeHTML(panelEl.innerHTML.trim());\n render = (container: HTMLElement) => {\n const wrapper = document.createElement('div');\n wrapper.innerHTML = content;\n container.appendChild(wrapper);\n return () => wrapper.remove();\n };\n }\n\n // Check if panel was already parsed\n const existingPanel = state.toolPanels.get(id);\n\n // Precedence (#370): a panel contributed by a plugin (or registered via the\n // grid API) owns its id. A light-DOM <tbw-grid-tool-panel> with the same id\n // is ignored so the owner can rely on its own DOM existing. We detect a\n // foreign owner by the id being present in toolPanels but NOT tracked as a\n // light-DOM id (which would mean this is just a re-parse of our own panel).\n if (existingPanel && !state.lightDomToolPanelIds.has(id)) {\n warnDiagnostic(\n TOOL_PANEL_DUPLICATE,\n `Tool panel \"${id}\" is already provided by a plugin or the grid API; ignoring the matching light-DOM <tbw-grid-tool-panel>.`,\n );\n panelEl.style.display = 'none';\n return;\n }\n\n // If already parsed and we have an adapter renderer, refresh the render\n // function and (potentially) re-read attributes from DOM (Angular may have\n // updated them after the initial parse). However, **only tear down the\n // already-rendered panel content** when something the user can observe has\n // actually changed:\n // - First time the adapter renderer attaches (we may have been using\n // the vanilla innerHTML fallback up to now)\n // - Any attribute the panel header renders from changed\n //\n // Otherwise the user's React/Vue/Angular component is already mounted in\n // the panel and is happily managing its own state. A teardown here unmounts\n // it (losing local state) and re-mounts it on next renderPanelContent —\n // which, for a scrollable panel, resets `scrollTop` to 0. This was the\n // cause of the \"tool panel scroll jumps to top on column visibility toggle\"\n // bug: every `grid.gridConfig = …` setter in the adapter eventually calls\n // back into parseLightDomToolPanels (via #applyGridConfigUpdate), and the\n // previous unconditional `cleanup()` here threw away the user's component\n // tree on every config sync.\n if (existingPanel) {\n if (adapterRenderer) {\n const firstAdapterAttach = !state.adapterBoundToolPanelIds.has(id);\n const attrsChanged =\n existingPanel.order !== order || existingPanel.icon !== icon || existingPanel.tooltip !== tooltip;\n\n // Always keep the render function fresh — adapter renderers capture\n // the current grid element / portal context and the most recent\n // children, so a stale reference would render against an old closure.\n existingPanel.render = render;\n existingPanel.order = order;\n existingPanel.icon = icon;\n existingPanel.tooltip = tooltip;\n state.adapterBoundToolPanelIds.add(id);\n\n if (firstAdapterAttach || attrsChanged) {\n const cleanup = state.panelCleanups.get(id);\n if (cleanup) {\n cleanup();\n state.panelCleanups.delete(id);\n }\n }\n }\n return;\n }\n\n // Register the tool panel\n const panel: ToolPanelDefinition = {\n id,\n title,\n icon,\n tooltip,\n order,\n render,\n };\n\n state.toolPanels.set(id, panel);\n state.lightDomToolPanelIds.add(id);\n if (adapterRenderer) {\n state.adapterBoundToolPanelIds.add(id);\n }\n\n // Hide the light DOM element\n panelEl.style.display = 'none';\n });\n}\n// #endregion\n\n// #region Event Handlers\n/**\n * Set up event listeners for shell toolbar buttons and accordion.\n */\nexport function setupShellEventListeners(\n renderRoot: Element,\n config: ShellConfig | undefined,\n state: ShellState,\n callbacks: {\n onPanelToggle: () => void;\n onSectionToggle: (sectionId: string) => void;\n /** Invoked when the in-panel close (✕) button is clicked. */\n onPanelClose?: () => void;\n },\n): void {\n const toolbar = renderRoot.querySelector('.tbw-shell-toolbar');\n if (toolbar) {\n toolbar.addEventListener('click', (e) => {\n const target = e.target as HTMLElement;\n\n // Handle single panel toggle button\n const panelToggle = target.closest('[data-panel-toggle]') as HTMLElement | null;\n if (panelToggle) {\n callbacks.onPanelToggle();\n return;\n }\n });\n }\n\n // In-panel close (✕) button — only present when the header bar is hidden.\n const panel = renderRoot.querySelector('.tbw-tool-panel');\n if (panel && callbacks.onPanelClose) {\n panel.addEventListener('click', (e) => {\n const target = e.target as HTMLElement;\n if (target.closest('[data-panel-close]')) {\n callbacks.onPanelClose!();\n }\n });\n }\n\n // Accordion header clicks\n const accordion = renderRoot.querySelector('.tbw-accordion');\n if (accordion) {\n accordion.addEventListener('click', (e) => {\n const target = e.target as HTMLElement;\n const header = target.closest('.tbw-accordion-header') as HTMLElement | null;\n if (header) {\n const section = header.closest('[data-section]') as HTMLElement | null;\n const sectionId = section?.getAttribute('data-section');\n if (sectionId) {\n callbacks.onSectionToggle(sectionId);\n }\n }\n });\n }\n}\n\n/**\n * Whether `node` belongs to `gridElement` — directly (light DOM) or via the\n * grid's shadow root (the node's root-node host is the grid). Used to scope\n * click-outside checks so one grid's listener never treats another grid's\n * toggle or panel as \"inside\" itself.\n */\nfunction isInGrid(node: Node, gridElement: Element): boolean {\n if (gridElement.contains(node)) return true;\n const root = node.getRootNode();\n return root instanceof ShadowRoot && root.host === gridElement;\n}\n\n/**\n * Set up a click-outside listener that closes the tool panel when the user\n * clicks anywhere in the document outside the tool panel itself.\n *\n * Only active when `config.toolPanel.closeOnClickOutside` is `true` AND the\n * panel is open. The listener is added on `document` `mousedown` (not `click`)\n * so it fires before focus changes and catches clicks anywhere in the window —\n * including outside the grid. Uses `composedPath()` so clicks inside the\n * grid's shadow DOM are correctly attributed (shadow-retargeted `event.target`\n * would otherwise always be the host element).\n *\n * @returns A cleanup function that removes the listener.\n */\nexport function setupClickOutsideDismiss(\n gridElement: Element,\n config: ShellConfig | undefined,\n state: ShellState,\n onClose: () => void,\n): () => void {\n const mode = config?.toolPanel?.mode;\n\n // Dropdown popovers always light-dismiss on outside click (standard dropdown\n // behavior), regardless of the `closeOnClickOutside` flag. Other modes only\n // dismiss when the consumer opts in.\n if (mode !== 'dropdown' && !config?.toolPanel?.closeOnClickOutside) {\n // eslint-disable-next-line @typescript-eslint/no-empty-function\n return () => {};\n }\n\n // In push mode the panel does not overlap grid content, so there is\n // no meaningful \"outside\" to click against. Treat the option as a no-op.\n if (mode === 'push') {\n // eslint-disable-next-line @typescript-eslint/no-empty-function\n return () => {};\n }\n\n const handler = (e: Event) => {\n if (!state.isPanelOpen) return;\n\n // Ignore clicks inside the tool panel itself, on the toggle button, or on\n // the dropdown's resolved anchor element (so a consumer's BYO trigger can\n // toggle without the outside-click handler racing the button's own click).\n // composedPath() pierces shadow boundaries so this works for both the\n // light-DOM toggle and the shadow-DOM panel.\n //\n // CRITICAL: the panel/toggle match MUST be scoped to THIS grid via\n // `isInGrid`. With multiple grids on one page, a click on another grid's\n // toggle/panel is an \"outside\" click for us and must still dismiss \\u2014\n // otherwise opening a dropdown in grid B leaves grid A's dropdown stuck\n // open (#375 follow-up).\n const anchorEl = state.dropdownAnchorEl;\n for (const node of e.composedPath()) {\n if (anchorEl != null && node === anchorEl) return;\n if (\n node instanceof Element &&\n (node.classList?.contains('tbw-tool-panel') || node.matches?.('[data-panel-toggle]')) &&\n isInGrid(node, gridElement)\n ) {\n return;\n }\n }\n\n onClose();\n };\n\n document.addEventListener('mousedown', handler);\n return () => document.removeEventListener('mousedown', handler);\n}\n\n/**\n * Set up an Escape-key listener that closes an open overlay tool panel.\n *\n * Active only in `overlay` mode (a push panel is a persistent sidebar, not a\n * transient overlay, so Esc does not dismiss it — the in-panel close button is\n * the affordance there). Yields to more specific handlers: if another listener\n * already called `preventDefault()` on the keydown (e.g. an active cell editor\n * cancelling an edit), the panel is left open.\n *\n * @returns A cleanup function that removes the listener.\n */\nexport function setupEscapeDismiss(\n config: ShellConfig | undefined,\n state: ShellState,\n onClose: () => void,\n): () => void {\n // Push panels are persistent — Esc only dismisses overlay panels.\n if (config?.toolPanel?.mode === 'push') {\n // eslint-disable-next-line @typescript-eslint/no-empty-function\n return () => {};\n }\n\n const handler = (e: KeyboardEvent) => {\n if (e.key !== 'Escape' || e.defaultPrevented) return;\n if (!state.isPanelOpen) return;\n onClose();\n };\n\n document.addEventListener('keydown', handler);\n return () => document.removeEventListener('keydown', handler);\n}\n\n/**\n * Set up resize handle for tool panel.\n * Returns a cleanup function to remove event listeners.\n */\nexport function setupToolPanelResize(\n renderRoot: Element,\n config: ShellConfig | undefined,\n onResize: (width: number) => void,\n): () => void {\n const panel = renderRoot.querySelector('.tbw-tool-panel') as HTMLElement | null;\n const handle = renderRoot.querySelector('[data-resize-handle]') as HTMLElement | null;\n const shellBody = renderRoot.querySelector('.tbw-shell-body') as HTMLElement | null;\n if (!panel || !handle || !shellBody) {\n // eslint-disable-next-line @typescript-eslint/no-empty-function\n return () => {};\n }\n\n const position = config?.toolPanel?.position ?? 'right';\n const minWidth = 200;\n\n let startX = 0;\n let startWidth = 0;\n let maxWidth = 0;\n let isResizing = false;\n\n const onMouseMove = (e: MouseEvent) => {\n if (!isResizing) return;\n e.preventDefault();\n\n // For right-positioned panel: dragging left (negative clientX change) should expand\n // For left-positioned panel: dragging right (positive clientX change) should expand\n const delta = position === 'left' ? e.clientX - startX : startX - e.clientX;\n const newWidth = Math.min(maxWidth, Math.max(minWidth, startWidth + delta));\n\n panel.style.width = `${newWidth}px`;\n };\n\n const onMouseUp = () => {\n if (!isResizing) return;\n isResizing = false;\n handle.classList.remove('resizing');\n panel.style.transition = ''; // Re-enable transition\n document.body.style.cursor = '';\n document.body.style.userSelect = '';\n\n // Get final width and notify\n const finalWidth = panel.getBoundingClientRect().width;\n onResize(finalWidth);\n\n document.removeEventListener('mousemove', onMouseMove);\n document.removeEventListener('mouseup', onMouseUp);\n };\n\n const onMouseDown = (e: MouseEvent) => {\n e.preventDefault();\n isResizing = true;\n startX = e.clientX;\n startWidth = panel.getBoundingClientRect().width;\n // Calculate max width dynamically based on grid container width\n maxWidth = shellBody.getBoundingClientRect().width - 20; // Leave 20px margin\n handle.classList.add('resizing');\n panel.style.transition = 'none'; // Disable transition for smooth resize\n document.body.style.cursor = 'col-resize';\n document.body.style.userSelect = 'none';\n\n document.addEventListener('mousemove', onMouseMove);\n document.addEventListener('mouseup', onMouseUp);\n };\n\n handle.addEventListener('mousedown', onMouseDown);\n\n // Return cleanup function\n return () => {\n handle.removeEventListener('mousedown', onMouseDown);\n document.removeEventListener('mousemove', onMouseMove);\n document.removeEventListener('mouseup', onMouseUp);\n };\n}\n// #endregion\n\n// #region Content Rendering\n/**\n * Render toolbar content (render functions) into toolbar slots.\n * All contents (config + API + light DOM) are now unified.\n */\nexport function renderCustomToolbarContents(\n renderRoot: Element,\n config: ShellConfig | undefined,\n state: ShellState,\n): void {\n // Merge config contents with state contents (same logic as renderShellHeader)\n const configContents = config?.header?.toolbarContents ?? [];\n const stateContents = [...state.toolbarContents.values()];\n const configIds = new Set(configContents.map((c) => c.id));\n const allContents = [...configContents];\n for (const content of stateContents) {\n if (!configIds.has(content.id)) {\n allContents.push(content);\n }\n }\n\n // Only process contents that need rendering (have render and cleanup not already set)\n for (const content of allContents) {\n // Skip if already rendered (cleanup exists)\n if (state.toolbarContentCleanups.has(content.id)) continue;\n if (!content.render) continue;\n\n const slot = renderRoot.querySelector(`[data-toolbar-content=\"${content.id}\"]`);\n if (!slot) continue;\n\n const cleanup = content.render(slot as HTMLElement);\n if (cleanup) {\n state.toolbarContentCleanups.set(content.id, cleanup);\n }\n }\n}\n\n/**\n * Render header content from plugins into the shell content area.\n * Also moves light DOM header content to the placeholder (once).\n */\nexport function renderHeaderContent(renderRoot: Element, state: ShellState): void {\n // Early exit if nothing to do (most common path after initial render)\n const hasLightDomContent = state.lightDomHeaderContent.length > 0 && !state.lightDomContentMoved;\n const hasPluginContent = state.headerContents.size > 0;\n if (!hasLightDomContent && !hasPluginContent) return;\n\n const contentArea = renderRoot.querySelector('.tbw-shell-content');\n if (!contentArea) return;\n\n // Move light DOM header content to placeholder - only once (perf optimization)\n if (hasLightDomContent) {\n for (const el of state.lightDomHeaderContent) {\n el.style.display = ''; // Show it (was hidden in the original container)\n contentArea.appendChild(el);\n }\n state.lightDomContentMoved = true;\n }\n\n // Sort by order\n const sortedContents = [...state.headerContents.values()].sort((a, b) => (a.order ?? 100) - (b.order ?? 100));\n\n for (const content of sortedContents) {\n // Clean up previous render if any\n const existingCleanup = state.headerContentCleanups.get(content.id);\n if (existingCleanup) {\n existingCleanup();\n state.headerContentCleanups.delete(content.id);\n }\n\n // Check if container already exists\n let container = contentArea.querySelector(`[data-header-content=\"${content.id}\"]`) as HTMLElement | null;\n if (!container) {\n container = document.createElement('div');\n container.setAttribute('data-header-content', content.id);\n contentArea.appendChild(container);\n }\n\n const cleanup = content.render(container);\n if (cleanup) {\n state.headerContentCleanups.set(content.id, cleanup);\n }\n }\n}\n\n/**\n * Render content for expanded accordion sections.\n * @param icons - Optional icons for expand/collapse chevrons (from grid config)\n */\nexport function renderPanelContent(\n renderRoot: Element,\n state: ShellState,\n _icons?: { expand?: IconValue; collapse?: IconValue },\n): void {\n if (!state.isPanelOpen) return;\n\n for (const [panelId, panel] of state.toolPanels) {\n const isExpanded = state.expandedSections.has(panelId);\n const section = renderRoot.querySelector(`[data-section=\"${panelId}\"]`);\n const contentArea = section?.querySelector('.tbw-accordion-content') as HTMLElement | null;\n\n if (!section || !contentArea) continue;\n\n // Update expanded state\n section.classList.toggle('expanded', isExpanded);\n const header = section.querySelector('.tbw-accordion-header');\n if (header) {\n header.setAttribute('aria-expanded', String(isExpanded));\n }\n // Don't swap chevron icon — CSS rotation handles expanded/collapsed state\n\n if (isExpanded) {\n // Check if content is already rendered\n if (contentArea.children.length === 0) {\n // Render panel content\n const cleanup = panel.render(contentArea);\n if (cleanup) {\n state.panelCleanups.set(panelId, cleanup);\n }\n }\n } else {\n // Clean up and clear content when collapsed\n const cleanup = state.panelCleanups.get(panelId);\n if (cleanup) {\n cleanup();\n state.panelCleanups.delete(panelId);\n }\n contentArea.innerHTML = '';\n }\n }\n}\n\n/**\n * Update toolbar button active states.\n */\nexport function updateToolbarActiveStates(renderRoot: Element, state: ShellState): void {\n // Update single panel toggle button\n const panelToggle = renderRoot.querySelector('[data-panel-toggle]');\n if (panelToggle) {\n panelToggle.classList.toggle('active', state.isPanelOpen);\n panelToggle.setAttribute('aria-pressed', String(state.isPanelOpen));\n }\n}\n\n/**\n * Update tool panel open/close state.\n */\nexport function updatePanelState(renderRoot: Element, state: ShellState): void {\n const panel = renderRoot.querySelector('.tbw-tool-panel') as HTMLElement | null;\n if (!panel) return;\n\n panel.classList.toggle('open', state.isPanelOpen);\n\n // Clear inline width when closing (resize sets inline style that overrides CSS)\n if (!state.isPanelOpen) {\n panel.style.width = '';\n }\n}\n\n/** Base CSS anchor name used to feature-detect CSS anchor positioning support. */\nconst DROPDOWN_ANCHOR_NAME = '--tbw-tool-panel-anchor';\n\n/**\n * Monotonic counter for minting unique per-grid dropdown anchor names. A shared\n * constant name collides when two grids on the same page each open a dropdown,\n * pairing one grid's popover with the other grid's trigger (#375 follow-up).\n */\nlet dropdownAnchorSeq = 0;\n\n/** Whether the current environment supports CSS anchor positioning. */\nexport function supportsAnchorPositioning(): boolean {\n return (\n typeof CSS !== 'undefined' &&\n typeof CSS.supports === 'function' &&\n CSS.supports('anchor-name', DROPDOWN_ANCHOR_NAME)\n );\n}\n\n/**\n * Position the dropdown popover with fixed coordinates derived from the\n * anchor's bounding rect. Used only when CSS anchor positioning is\n * unsupported; otherwise placement is driven entirely by `shell.css`.\n */\nfunction positionDropdownFallback(panel: HTMLElement, anchorEl: HTMLElement, corner: boolean): void {\n const rect = anchorEl.getBoundingClientRect();\n const position = panel.getAttribute('data-position') ?? 'right';\n panel.style.position = 'fixed';\n // `corner` anchors to the top of the grid (top corner); otherwise drop below\n // the trigger button.\n panel.style.top = `${Math.round((corner ? rect.top : rect.bottom) + 4)}px`;\n if (position === 'left') {\n panel.style.left = `${Math.round(rect.left)}px`;\n panel.style.right = 'auto';\n } else {\n const viewportWidth = typeof window !== 'undefined' ? window.innerWidth : 0;\n panel.style.left = 'auto';\n panel.style.right = `${Math.round(viewportWidth - rect.right)}px`;\n }\n}\n\n/**\n * Show the tool panel as an anchored dropdown popover (`mode: 'dropdown'`).\n *\n * Reuses the existing `.tbw-tool-panel` aside (the full accordion) and shows it\n * in the top layer via the native Popover API. Placement is handled by CSS\n * anchor positioning when supported, with a JS bounding-rect fallback otherwise.\n *\n * @param anchorEl - Element the popover anchors to.\n * @param corner - When `true`, anchor to the top corner of `anchorEl` (grid\n * corner fallback); otherwise drop directly below it (toggle / custom button).\n */\nexport function showToolPanelDropdown(\n renderRoot: Element,\n state: ShellState,\n anchorEl: HTMLElement,\n corner: boolean,\n): void {\n const panel = renderRoot.querySelector('.tbw-tool-panel') as (HTMLElement & { showPopover?: () => void }) | null;\n if (!panel) return;\n\n state.dropdownAnchorEl = anchorEl;\n panel.dataset.anchor = corner ? 'corner' : 'below';\n\n // Show in the top layer. Guard for environments (older browsers / test DOM)\n // without the native Popover API.\n if (panel.hasAttribute('popover') && typeof panel.showPopover === 'function') {\n try {\n panel.showPopover();\n } catch {\n // Already open or not connected — ignore.\n }\n }\n\n if (supportsAnchorPositioning()) {\n // CSS drives placement; just wire up the anchor-name pairing. Mint a unique\n // name per show so multiple grids (or repeated opens) on one page never\n // collide on a shared anchor name — a collision positions this popover\n // against another grid's trigger instead of its own.\n const anchorName = `${DROPDOWN_ANCHOR_NAME}-${++dropdownAnchorSeq}`;\n state.dropdownAnchorName = anchorName;\n anchorEl.style.setProperty('anchor-name', anchorName);\n panel.style.setProperty('position-anchor', anchorName);\n panel.style.removeProperty('top');\n panel.style.removeProperty('left');\n panel.style.removeProperty('right');\n } else {\n positionDropdownFallback(panel, anchorEl, corner);\n }\n}\n\n/**\n * Hide the dropdown popover and clear all anchor wiring set by\n * {@link showToolPanelDropdown}.\n */\nexport function hideToolPanelDropdown(renderRoot: Element, state: ShellState): void {\n const panel = renderRoot.querySelector('.tbw-tool-panel') as (HTMLElement & { hidePopover?: () => void }) | null;\n if (panel) {\n if (panel.hasAttribute('popover') && typeof panel.hidePopover === 'function') {\n try {\n panel.hidePopover();\n } catch {\n // Not currently open — ignore.\n }\n }\n panel.style.removeProperty('position-anchor');\n panel.style.removeProperty('position');\n panel.style.removeProperty('top');\n panel.style.removeProperty('left');\n panel.style.removeProperty('right');\n delete panel.dataset.anchor;\n }\n if (state.dropdownAnchorEl) {\n state.dropdownAnchorEl.style.removeProperty('anchor-name');\n state.dropdownAnchorEl = null;\n }\n state.dropdownAnchorName = null;\n}\n\n/**\n * Re-pair an OPEN dropdown's existing anchor onto a freshly resolved trigger\n * IN PLACE — reusing the already-minted `anchor-name` and leaving the popover\n * shown — instead of hiding and re-showing it.\n *\n * WHY: when an outer framework (e.g. a React column `headerRenderer`) recreates\n * the `[data-panel-toggle]` trigger on a column toggle, the new node lacks the\n * `anchor-name`, so the popover's `position-anchor` no longer resolves and the\n * browser drops it to the corner. Hiding + re-showing (the heavier\n * {@link showToolPanelDropdown} path) mints a NEW name and re-enters the\n * Popover API, which flashes the popover to the corner for a frame. Moving the\n * SAME name to the new node keeps the popover visually anchored with no flash.\n *\n * Returns `true` when the re-pair was applied. The caller must ensure CSS\n * anchor positioning is supported (see {@link supportsAnchorPositioning});\n * otherwise the fixed-coordinate fallback in {@link showToolPanelDropdown} must\n * be used instead.\n */\nexport function repairDropdownAnchor(renderRoot: Element, state: ShellState, trigger: HTMLElement): boolean {\n const anchorName = state.dropdownAnchorName;\n if (!anchorName) return false;\n const panel = renderRoot.querySelector('.tbw-tool-panel') as (HTMLElement & { showPopover?: () => void }) | null;\n if (!panel) return false;\n const prev = state.dropdownAnchorEl;\n if (prev && prev !== trigger) prev.style.removeProperty('anchor-name');\n trigger.style.setProperty('anchor-name', anchorName);\n panel.style.setProperty('position-anchor', anchorName);\n panel.style.removeProperty('top');\n panel.style.removeProperty('left');\n panel.style.removeProperty('right');\n panel.dataset.anchor = 'below';\n state.dropdownAnchorEl = trigger;\n // A structural rebuild (`rebuildShellDOM`) creates a FRESH `.tbw-tool-panel`\n // that is not in the top layer — only its `.open` class arm renders it, which\n // can be clipped / mis-stacked. Re-assert the popover idempotently so both the\n // non-structural path (panel already open → no-op via the catch) and the\n // structural-rebuild path (fresh panel → shown) end up in the top layer.\n if (panel.hasAttribute('popover') && typeof panel.showPopover === 'function') {\n try {\n panel.showPopover();\n } catch {\n // Already open or not connected — ignore.\n }\n }\n return true;\n}\n\n/**\n * Prepare shell state for a full re-render.\n * Runs cleanup functions so content (toolbar buttons, panels, headers)\n * can be restored to their original containers and re-rendered into new DOM.\n */\nexport function prepareForRerender(state: ShellState): void {\n // Run cleanups for toolbar contents (moves elements back to original containers)\n for (const cleanup of state.toolbarContentCleanups.values()) {\n cleanup();\n }\n state.toolbarContentCleanups.clear();\n\n // Run cleanups for panel contents (old DOM will be destroyed)\n for (const cleanup of state.panelCleanups.values()) {\n cleanup();\n }\n state.panelCleanups.clear();\n\n // Run cleanups for header contents (old DOM will be destroyed)\n for (const cleanup of state.headerContentCleanups.values()) {\n cleanup();\n }\n state.headerContentCleanups.clear();\n\n // Allow light DOM content to be re-moved into new DOM\n state.lightDomContentMoved = false;\n}\n\n/**\n * Cleanup all shell state when grid disconnects.\n */\nexport function cleanupShellState(state: ShellState): void {\n // Clean up header content\n for (const cleanup of state.headerContentCleanups.values()) {\n cleanup();\n }\n state.headerContentCleanups.clear();\n\n // Clean up panel content\n for (const cleanup of state.panelCleanups.values()) {\n cleanup();\n }\n state.panelCleanups.clear();\n\n // Clean up toolbar contents\n for (const cleanup of state.toolbarContentCleanups.values()) {\n cleanup();\n }\n state.toolbarContentCleanups.clear();\n\n // Call onDestroy for all toolbar contents\n for (const content of state.toolbarContents.values()) {\n content.onDestroy?.();\n }\n\n // Invoke onClose for all open panels\n if (state.isPanelOpen) {\n for (const sectionId of state.expandedSections) {\n const panel = state.toolPanels.get(sectionId);\n panel?.onClose?.();\n }\n }\n\n // Reset panel state\n state.isPanelOpen = false;\n state.expandedSections.clear();\n\n // Clear registrations\n state.toolPanels.clear();\n state.headerContents.clear();\n state.toolbarContents.clear();\n state.lightDomHeaderContent = [];\n\n // Clear light DOM tracking sets (allow re-parsing)\n state.lightDomToolPanelIds.clear();\n state.adapterBoundToolPanelIds.clear();\n state.lightDomToolbarContentIds.clear();\n\n // Reset move tracking flag (allow re-initialization)\n state.lightDomContentMoved = false;\n}\n// #endregion\n\n// #region Grid HTML Templates\n/**\n * Core grid content HTML template.\n * Uses faux scrollbar pattern for smooth virtualized scrolling.\n */\nexport const GRID_CONTENT_HTML = `\n <div class=\"tbw-scroll-area\">\n <div class=\"rows-body-wrapper\">\n <div class=\"rows-body\" role=\"grid\">\n <div class=\"header\" role=\"rowgroup\">\n <div class=\"header-row\" role=\"row\" part=\"header-row\"></div>\n </div>\n <div class=\"rows-container\" role=\"presentation\">\n <div class=\"rows-viewport\" role=\"presentation\">\n <div class=\"rows\"></div>\n </div>\n </div>\n </div>\n </div>\n </div>\n <div class=\"faux-vscroll\">\n <div class=\"faux-vscroll-spacer\"></div>\n </div>\n <div class=\"tbw-sr-only\" aria-live=\"polite\" aria-atomic=\"true\"></div>\n`;\n// #endregion\n\n// #region DOM Construction\nimport { GridClasses } from '../../core/constants';\nimport {\n buildGridDOM,\n buildShellBody,\n buildShellHeader,\n type ShellBodyOptions,\n type ShellHeaderOptions,\n} from '../../core/internal/dom-builder';\n\n/**\n * Build ShellHeaderOptions and ShellBodyOptions from shell config and runtime state.\n * Shared by buildGridDOMIntoElement and rebuildShellDOM to avoid duplication.\n */\nfunction buildShellOptions(\n shellConfig: ShellConfig,\n runtimeState: { isPanelOpen: boolean; expandedSections: Set<string> },\n icons?: { toolPanel?: IconValue; expand?: IconValue; collapse?: IconValue },\n): { headerOptions: ShellHeaderOptions; bodyOptions: ShellBodyOptions } {\n const toolPanelIcon = icons?.toolPanel !== undefined ? iconToString(icons.toolPanel) : undefined;\n const expandIcon = icons?.expand !== undefined ? iconToString(icons.expand) : undefined;\n const collapseIcon = icons?.collapse !== undefined ? iconToString(icons.collapse) : undefined;\n\n const allContents = shellConfig.header?.toolbarContents ?? [];\n const sortedContents = [...allContents].sort((a, b) => (a.order ?? 0) - (b.order ?? 0));\n\n const allPanels = shellConfig.toolPanels ?? [];\n const sortedPanels = [...allPanels].sort((a, b) => (a.order ?? 100) - (b.order ?? 100));\n\n return {\n headerOptions: {\n title: shellConfig.header?.title ?? undefined,\n hasPanels: sortedPanels.length > 0,\n isPanelOpen: runtimeState.isPanelOpen,\n toolPanelIcon,\n showToggle: shellConfig.header?.toolPanelToggle !== false && shellConfig.toolPanel?.locked !== true,\n configButtons: sortedContents.map((c) => ({\n id: c.id,\n hasElement: false,\n hasRender: !!c.render,\n })),\n apiButtons: [],\n },\n bodyOptions: {\n position: shellConfig.toolPanel?.position ?? 'right',\n mode: shellConfig.toolPanel?.mode ?? 'overlay',\n isPanelOpen: runtimeState.isPanelOpen,\n expandIcon,\n collapseIcon,\n // Render an in-panel close (✕) button when the header bar is hidden, so\n // the panel can be dismissed without the (absent) built-in toggle. A\n // locked panel cannot be closed, so no button in that case. Dropdown mode\n // is excluded: the popover light-dismisses on Esc / click-outside, so a\n // ✕ would be redundant clutter.\n showCloseButton:\n shellConfig.header?.visible === false &&\n shellConfig.toolPanel?.locked !== true &&\n (shellConfig.toolPanel?.mode ?? 'overlay') !== 'dropdown',\n panels: sortedPanels.map((p) => ({\n id: p.id,\n title: p.title,\n icon: iconToString(p.icon),\n isExpanded: runtimeState.expandedSections.has(p.id),\n })),\n },\n };\n}\n\n/**\n * Build the complete grid DOM structure using direct DOM construction.\n * This is 2-3x faster than innerHTML for initial render.\n *\n * @param renderRoot - The element to render into (will be cleared)\n * @param shellConfig - Shell configuration\n * @param runtimeState - Runtime shell state\n * @param icons - Optional icons\n * @returns Whether shell is active (for post-render setup)\n */\nexport function buildGridDOMIntoElement(\n renderRoot: Element,\n shellConfig: ShellConfig | undefined,\n runtimeState: { isPanelOpen: boolean; expandedSections: Set<string> },\n icons?: { toolPanel?: IconValue; expand?: IconValue; collapse?: IconValue },\n): boolean {\n const hasShell = shouldRenderShellHeader(shellConfig);\n\n // Preserve all direct child nodes EXCEPT the grid's own `.tbw-grid-root`\n // (which we are about to rebuild). This keeps user/framework light-DOM\n // intact in its original order — including non-element nodes like comment\n // anchors that Angular's `@if`/`@for` and other structural directives use\n // as positional markers. Filtering by element-tag selectors would discard\n // those anchors and break re-renders of the conditional content.\n const preservedNodes: Node[] = [];\n for (let n: Node | null = renderRoot.firstChild; n; n = n.nextSibling) {\n if (n.nodeType === 1 && (n as Element).classList.contains(GridClasses.ROOT)) continue;\n preservedNodes.push(n);\n }\n\n // Clear existing content (this would delete light DOM nodes, so we preserved them first)\n renderRoot.replaceChildren();\n\n // Re-append preserved nodes in original order (hidden elements are used for config).\n // IMPORTANT: These are prepended before .tbw-grid-root, so `renderRoot.children[0]`\n // is NOT the grid root. Use `querySelector('.tbw-grid-root')` instead.\n for (const n of preservedNodes) {\n renderRoot.appendChild(n);\n }\n\n if (hasShell) {\n const { headerOptions, bodyOptions } = buildShellOptions(shellConfig!, runtimeState, icons);\n const shellHeader = shouldRenderHeaderBar(shellConfig) ? buildShellHeader(headerOptions) : undefined;\n const shellBody = buildShellBody(bodyOptions);\n\n const fragment = buildGridDOM({\n hasShell: true,\n shellHeader,\n shellBody,\n });\n renderRoot.appendChild(fragment);\n } else {\n const fragment = buildGridDOM({ hasShell: false });\n renderRoot.appendChild(fragment);\n }\n\n return hasShell;\n}\n\n/**\n * Surgically rebuild only the shell wrapper (header + tool panel) while\n * preserving the existing `.tbw-grid-root` element and its `.tbw-grid-content`\n * child with all descendants and event listeners intact.\n *\n * This avoids the full `replaceChildren()` nuke that `buildGridDOMIntoElement`\n * performs, so event listeners bound to `.tbw-grid-root` or its grid content\n * descendants (e.g. tooltip's delegated mouseover) remain intact.\n *\n * If no existing `.tbw-grid-root` is found (first render), falls back to\n * `buildGridDOMIntoElement` for a full rebuild.\n */\nexport function rebuildShellDOM(\n renderRoot: Element,\n shellConfig: ShellConfig | undefined,\n runtimeState: { isPanelOpen: boolean; expandedSections: Set<string> },\n icons?: { toolPanel?: IconValue; expand?: IconValue; collapse?: IconValue },\n): boolean {\n // Find the existing grid root and content to preserve\n const existingRoot = renderRoot.querySelector(`.${GridClasses.ROOT}`) as HTMLElement | null;\n const existingContent = existingRoot?.querySelector('.tbw-grid-content');\n\n // If there's no existing root, fall back to full rebuild (first render)\n if (!existingRoot || !existingContent) {\n return buildGridDOMIntoElement(renderRoot, shellConfig, runtimeState, icons);\n }\n\n // Detach grid content so it survives the child replacement\n existingContent.remove();\n\n const hasShell = shouldRenderShellHeader(shellConfig);\n\n // Clear the root's children (shell header, shell body, etc.) but keep the root element itself\n existingRoot.replaceChildren();\n\n if (hasShell) {\n existingRoot.className = `${GridClasses.ROOT} has-shell`;\n\n const { headerOptions, bodyOptions } = buildShellOptions(shellConfig!, runtimeState, icons);\n const shellHeader = shouldRenderHeaderBar(shellConfig) ? buildShellHeader(headerOptions) : undefined;\n const shellBody = buildShellBody(bodyOptions);\n\n // Replace the freshly cloned grid content with the preserved one\n const freshContent = shellBody.querySelector('.tbw-grid-content');\n if (freshContent) {\n freshContent.replaceWith(existingContent);\n }\n\n if (shellHeader) existingRoot.appendChild(shellHeader);\n existingRoot.appendChild(shellBody);\n } else {\n // No shell — place content directly in root\n existingRoot.className = GridClasses.ROOT;\n existingRoot.appendChild(existingContent);\n }\n\n return hasShell;\n}\n// #endregion\n","/**\n * Shell Controller - Tool panel and header content orchestration.\n *\n * Extracted from shell.ts to isolate the stateful controller logic\n * from the rendering/parsing infrastructure.\n *\n * The ShellController manages:\n * - Tool panel open/close/toggle state\n * - Accordion section expand/collapse\n * - Registration of tool panels, header content, and toolbar content\n */\n\nimport {\n HEADER_CONTENT_DUPLICATE,\n NO_TOOL_PANELS,\n TOOL_PANEL_DUPLICATE,\n TOOL_PANEL_NOT_FOUND,\n TOOLBAR_CONTENT_DUPLICATE,\n warnDiagnostic,\n} from '../../core/internal/diagnostics';\nimport type { GridElement } from '../../core/plugin/base-plugin';\nimport type { InternalGrid } from '../../core/types';\nimport {\n hideToolPanelDropdown,\n renderHeaderContent,\n renderPanelContent,\n repairDropdownAnchor,\n showToolPanelDropdown,\n supportsAnchorPositioning,\n updatePanelState,\n updateToolbarActiveStates,\n type ShellState,\n} from './shell';\nimport type {\n HeaderContentDefinition,\n OpenToolPanelOptions,\n ToolbarContentDefinition,\n ToolPanelDefinition,\n} from './types';\n\n// #region ShellController\n/**\n * Controller interface for managing shell/tool panel behavior.\n */\nexport interface ShellController {\n /** Whether the shell has been initialized */\n readonly isInitialized: boolean;\n /** Set the initialized state */\n setInitialized(value: boolean): void;\n /** Whether the tool panel is currently open */\n readonly isPanelOpen: boolean;\n /** Get IDs of expanded accordion sections */\n readonly expandedSections: string[];\n /**\n * Open the tool panel.\n * @param panelId - Optional ID of the section to expand on open. When provided,\n * takes precedence over `shell.toolPanel.defaultOpen`. If the panel is already\n * open with a different section expanded, switches to the requested section.\n * If the panel ID is not registered, a `TBW072` warning is emitted and the\n * call falls back to default behavior (auto-expand `defaultOpen` or first by order).\n * @param options - Optional open options. In `mode: 'dropdown'`, `options.anchor`\n * sets the element the popover anchors to (see {@link OpenToolPanelOptions}).\n */\n openToolPanel(panelId?: string, options?: OpenToolPanelOptions): void;\n /** Close the tool panel */\n closeToolPanel(): void;\n /**\n * Toggle the tool panel.\n * @param options - Optional open options forwarded to {@link openToolPanel}\n * when the panel is currently closed (e.g. `anchor` for dropdown mode).\n */\n toggleToolPanel(options?: OpenToolPanelOptions): void;\n /**\n * Re-pair an open dropdown popover onto its (possibly recreated) trigger\n * after a re-render (`mode: 'dropdown'` only). A column toggle / reorder may\n * recreate the custom column-header button the popover was anchored to,\n * leaving the popover's `position-anchor` dangling so the browser drops it to\n * the corner.\n *\n * Behaviour:\n * - intact pairing → no-op (cheap fast-path, safe to call every render);\n * - a live `[data-panel-toggle]` exists → re-pair the existing anchor onto it\n * IN PLACE (no popover re-show → no flash), or a fixed-coord re-show when\n * CSS anchor positioning is unavailable;\n * - corner-origin dropdown with no trigger → keep it pinned to the corner;\n * - trigger-origin dropdown whose trigger is transiently absent → leave the\n * popover at its last position (the shell's MutationObserver re-pairs it\n * before paint once the trigger reappears), rather than flashing to corner.\n *\n * No-op when the panel is closed or the mode is not `'dropdown'`.\n */\n reanchorOpenDropdown(): void;\n /** Toggle an accordion section */\n toggleToolPanelSection(sectionId: string): void;\n /** Get registered tool panels */\n getToolPanels(): ToolPanelDefinition[];\n /** Register a tool panel */\n registerToolPanel(panel: ToolPanelDefinition): void;\n /** Unregister a tool panel */\n unregisterToolPanel(panelId: string): void;\n /** Get registered header contents */\n getHeaderContents(): HeaderContentDefinition[];\n /** Register header content */\n registerHeaderContent(content: HeaderContentDefinition): void;\n /** Unregister header content */\n unregisterHeaderContent(contentId: string): void;\n /** Get all registered toolbar contents */\n getToolbarContents(): ToolbarContentDefinition[];\n /** Register toolbar content */\n registerToolbarContent(content: ToolbarContentDefinition): void;\n /** Unregister toolbar content */\n unregisterToolbarContent(contentId: string): void;\n}\n\n/**\n * The slice of the grid the shell controller needs: the plugin-facing\n * {@link GridElement} surface plus a few internal seams (`id`, `_emit`,\n * `_accordionIcons`, optional `refreshShellHeader`). Declaring it as\n * `GridElement & Pick<InternalGrid, …>` lets the {@link ShellPlugin} pass its\n * `GridElement`-typed `grid` with a plain downcast — no `as unknown as` — while\n * keeping core free of any shell-specific grid surface (extraction #370).\n */\nexport type ShellControllerGrid = GridElement &\n Pick<InternalGrid, 'id' | '_emit' | '_accordionIcons' | 'refreshShellHeader'>;\n\n/**\n * Create a ShellController instance.\n * The controller encapsulates all tool panel orchestration logic.\n */\nexport function createShellController(state: ShellState, grid: ShellControllerGrid): ShellController {\n let initialized = false;\n\n /**\n * Resolve the element a dropdown popover should anchor to. Priority:\n * explicit `anchor` option → built-in toolbar toggle button → grid host\n * corner fallback.\n */\n function resolveDropdownAnchor(explicit?: HTMLElement): { el: HTMLElement; corner: boolean } {\n if (explicit) return { el: explicit, corner: false };\n const toggle = grid._renderRoot.querySelector('[data-panel-toggle]') as HTMLElement | null;\n if (toggle) return { el: toggle, corner: false };\n return { el: grid._hostElement, corner: true };\n }\n\n const controller: ShellController = {\n get isInitialized() {\n return initialized;\n },\n setInitialized(value: boolean) {\n initialized = value;\n },\n\n get isPanelOpen() {\n return state.isPanelOpen;\n },\n\n get expandedSections() {\n return [...state.expandedSections];\n },\n\n openToolPanel(panelId?: string, options?: OpenToolPanelOptions) {\n if (state.toolPanels.size === 0) {\n warnDiagnostic(NO_TOOL_PANELS, 'No tool panels registered', grid.id);\n return;\n }\n\n // Validate explicit panelId; on unknown id, warn and fall back to default behavior.\n let targetSectionId: string | undefined;\n if (panelId !== undefined) {\n if (state.toolPanels.has(panelId)) {\n targetSectionId = panelId;\n } else {\n warnDiagnostic(TOOL_PANEL_NOT_FOUND, `Tool panel \"${panelId}\" not found`, grid.id);\n }\n }\n\n // Fast path: panel already open and requested section already expanded → no-op.\n if (state.isPanelOpen && targetSectionId && state.expandedSections.has(targetSectionId)) {\n return;\n }\n\n // Panel already open but a different section is expanded → switch sections\n // (delegate to toggleToolPanelSection for accordion exclusivity + event emission).\n if (state.isPanelOpen && targetSectionId && !state.expandedSections.has(targetSectionId)) {\n controller.toggleToolPanelSection(targetSectionId);\n return;\n }\n\n if (state.isPanelOpen) return;\n\n state.isPanelOpen = true;\n\n // Decide initial expanded section:\n // 1. Explicit panelId argument (already validated above).\n // 2. shell.toolPanel.defaultOpen if it matches a registered panel.\n // 3. First panel by `order`.\n if (state.expandedSections.size === 0 && state.toolPanels.size > 0) {\n if (targetSectionId) {\n state.expandedSections.add(targetSectionId);\n } else {\n const defaultOpenId = grid.effectiveConfig?.shell?.toolPanel?.defaultOpen;\n if (defaultOpenId && state.toolPanels.has(defaultOpenId)) {\n state.expandedSections.add(defaultOpenId);\n } else {\n const sortedPanels = [...state.toolPanels.values()].sort((a, b) => (a.order ?? 100) - (b.order ?? 100));\n const firstPanel = sortedPanels[0];\n if (firstPanel) {\n state.expandedSections.add(firstPanel.id);\n }\n }\n }\n } else if (targetSectionId && !state.expandedSections.has(targetSectionId)) {\n // Some other section was already pre-expanded (e.g. via config); switch to requested.\n for (const id of [...state.expandedSections]) {\n state.expandedSections.delete(id);\n }\n state.expandedSections.add(targetSectionId);\n }\n\n // Update UI\n const shadow = grid._renderRoot;\n updateToolbarActiveStates(shadow, state);\n updatePanelState(shadow, state);\n\n // Render accordion sections\n renderPanelContent(shadow, state, grid._accordionIcons);\n\n // In dropdown mode, show the panel as an anchored popover in the top layer.\n if (grid.effectiveConfig?.shell?.toolPanel?.mode === 'dropdown') {\n const { el, corner } = resolveDropdownAnchor(options?.anchor);\n showToolPanelDropdown(shadow, state, el, corner);\n }\n\n // Emit event\n grid._emit('tool-panel-open', { sections: controller.expandedSections });\n },\n\n closeToolPanel() {\n if (!state.isPanelOpen) return;\n // When the tool panel is locked open, programmatic and user close\n // requests become no-ops. Accordion sections can still toggle.\n if (grid.effectiveConfig?.shell?.toolPanel?.locked) return;\n\n // Clean up all panel content\n for (const cleanup of state.panelCleanups.values()) {\n cleanup();\n }\n state.panelCleanups.clear();\n\n // Call onClose for all panels\n for (const panel of state.toolPanels.values()) {\n panel.onClose?.();\n }\n\n state.isPanelOpen = false;\n\n // Update UI\n const shadow = grid._renderRoot;\n updateToolbarActiveStates(shadow, state);\n updatePanelState(shadow, state);\n\n // In dropdown mode, hide the popover and clear anchor wiring.\n if (grid.effectiveConfig?.shell?.toolPanel?.mode === 'dropdown') {\n hideToolPanelDropdown(shadow, state);\n }\n\n // Emit event\n grid._emit('tool-panel-close', {});\n },\n\n toggleToolPanel(options?: OpenToolPanelOptions) {\n if (state.isPanelOpen) {\n controller.closeToolPanel();\n } else {\n controller.openToolPanel(undefined, options);\n }\n },\n\n reanchorOpenDropdown() {\n if (!state.isPanelOpen) return;\n if (grid.effectiveConfig?.shell?.toolPanel?.mode !== 'dropdown') return;\n const stored = state.dropdownAnchorEl;\n // Idempotent fast-path: the anchor element is still connected AND still\n // carries the minted `anchor-name`, so the pairing is intact — nothing to\n // do. This keeps the hook cheap when it runs on the `afterRender` path\n // (which fires on every render, e.g. a column toggle), only doing real\n // work when the trigger was actually recreated/detached.\n if (\n stored &&\n stored.isConnected &&\n state.dropdownAnchorName &&\n stored.style.getPropertyValue('anchor-name') === state.dropdownAnchorName\n ) {\n return;\n }\n // Idempotent fast-path (fixed-coordinate fallback mode — CSS anchor\n // positioning unsupported, so `dropdownAnchorName` is never minted). Here\n // re-anchoring means a `getBoundingClientRect()` + re-show on every\n // render/mutation, which is wasteful when nothing was recreated. Skip when\n // the stored trigger is still the SAME connected node (not the corner\n // anchor) AND the CURRENT panel was already positioned (`data-anchor` set).\n // A structural rebuild produces a FRESH `.tbw-tool-panel` with no\n // `data-anchor`, so this never short-circuits the case that still needs a\n // re-show.\n if (!state.dropdownAnchorName && stored && stored.isConnected && stored !== grid._hostElement) {\n const panel = grid._renderRoot.querySelector('.tbw-tool-panel') as HTMLElement | null;\n if (panel?.dataset.anchor === 'below') return;\n }\n // Re-pair to a LIVE custom trigger (`[data-panel-toggle]`) whenever one\n // exists. Prefer the cheap in-place repair: it reuses the already-minted\n // `anchor-name`, keeps the popover shown, and never re-enters the Popover\n // API — so the popover stays visually anchored with NO flash. Falls back\n // to a fixed-coordinate re-show only when CSS anchor positioning is\n // unavailable.\n const fresh = grid._renderRoot.querySelector('[data-panel-toggle]') as HTMLElement | null;\n if (fresh && fresh !== grid._hostElement) {\n if (\n state.dropdownAnchorName &&\n supportsAnchorPositioning() &&\n repairDropdownAnchor(grid._renderRoot, state, fresh)\n ) {\n return;\n }\n showToolPanelDropdown(grid._renderRoot, state, fresh, false);\n return;\n }\n // No live trigger.\n if (stored === grid._hostElement) {\n // Corner-origin dropdown (opened without a custom trigger): keep it\n // pinned to the grid corner across re-renders.\n showToolPanelDropdown(grid._renderRoot, state, grid._hostElement, true);\n return;\n }\n // Trigger-origin dropdown whose trigger is transiently absent (an outer\n // framework, e.g. React, recreates it on a separate commit) — or was\n // permanently removed. Do NOT flash the popover to the corner: leave it\n // at its last position. When the trigger reappears, the shell's\n // MutationObserver re-pairs it before paint (see ShellPlugin).\n },\n\n toggleToolPanelSection(sectionId: string) {\n const panel = state.toolPanels.get(sectionId);\n if (!panel) {\n warnDiagnostic(TOOL_PANEL_NOT_FOUND, `Tool panel section \"${sectionId}\" not found`, grid.id);\n return;\n }\n\n // Don't allow toggling when there's only one panel (it should stay expanded)\n if (state.toolPanels.size === 1) {\n return;\n }\n\n const shadow = grid._renderRoot;\n const isExpanded = state.expandedSections.has(sectionId);\n\n if (isExpanded) {\n // Collapsing current section\n const cleanup = state.panelCleanups.get(sectionId);\n if (cleanup) {\n cleanup();\n state.panelCleanups.delete(sectionId);\n }\n panel.onClose?.();\n state.expandedSections.delete(sectionId);\n updateAccordionSectionState(shadow, sectionId, false);\n } else {\n // Expanding - first collapse all others (exclusive accordion)\n for (const [otherId, otherPanel] of state.toolPanels) {\n if (otherId !== sectionId && state.expandedSections.has(otherId)) {\n const cleanup = state.panelCleanups.get(otherId);\n if (cleanup) {\n cleanup();\n state.panelCleanups.delete(otherId);\n }\n otherPanel.onClose?.();\n state.expandedSections.delete(otherId);\n updateAccordionSectionState(shadow, otherId, false);\n // Clear content of collapsed section\n const contentEl = shadow.querySelector(`[data-section=\"${otherId}\"] .tbw-accordion-content`);\n if (contentEl) contentEl.innerHTML = '';\n }\n }\n // Now expand the target section\n state.expandedSections.add(sectionId);\n updateAccordionSectionState(shadow, sectionId, true);\n renderAccordionSectionContent(shadow, state, sectionId);\n }\n\n // Emit event\n grid._emit('tool-panel-section-toggle', { id: sectionId, expanded: !isExpanded });\n },\n\n getToolPanels() {\n return [...state.toolPanels.values()];\n },\n\n registerToolPanel(panel: ToolPanelDefinition) {\n if (state.toolPanels.has(panel.id)) {\n warnDiagnostic(TOOL_PANEL_DUPLICATE, `Tool panel \"${panel.id}\" already registered`, grid.id);\n return;\n }\n state.toolPanels.set(panel.id, panel);\n\n // Refresh when already rendered, OR bootstrap the chrome when the shell is\n // connected but has not rendered yet (an opt-in `features: { shell: true }`\n // shell with no other content renders no chrome — registering the first\n // tool panel must build it). `refreshShellHeader` self-guards on\n // `isConnected` and batches, so a pre-connect call is a harmless no-op.\n if (initialized || grid._hostElement.isConnected) {\n grid.refreshShellHeader?.();\n }\n },\n\n unregisterToolPanel(panelId: string) {\n // Close panel if open and this section is expanded\n if (state.expandedSections.has(panelId)) {\n const cleanup = state.panelCleanups.get(panelId);\n if (cleanup) {\n cleanup();\n state.panelCleanups.delete(panelId);\n }\n state.expandedSections.delete(panelId);\n }\n\n state.toolPanels.delete(panelId);\n\n if (initialized || grid._hostElement.isConnected) {\n grid.refreshShellHeader?.();\n }\n },\n\n getHeaderContents() {\n return [...state.headerContents.values()];\n },\n\n registerHeaderContent(content: HeaderContentDefinition) {\n if (state.headerContents.has(content.id)) {\n warnDiagnostic(HEADER_CONTENT_DUPLICATE, `Header content \"${content.id}\" already registered`, grid.id);\n return;\n }\n state.headerContents.set(content.id, content);\n\n if (initialized) {\n renderHeaderContent(grid._renderRoot, state);\n } else if (grid._hostElement.isConnected) {\n // Connected opt-in shell with no chrome yet — bootstrap it so the header\n // content has a `.tbw-shell-header` to render into.\n grid.refreshShellHeader?.();\n }\n },\n\n unregisterHeaderContent(contentId: string) {\n // Clean up\n const cleanup = state.headerContentCleanups.get(contentId);\n if (cleanup) {\n cleanup();\n state.headerContentCleanups.delete(contentId);\n }\n\n // Call onDestroy\n const content = state.headerContents.get(contentId);\n content?.onDestroy?.();\n\n state.headerContents.delete(contentId);\n\n // Remove DOM element\n const el = grid._renderRoot.querySelector(`[data-header-content=\"${contentId}\"]`);\n el?.remove();\n },\n\n getToolbarContents() {\n return [...state.toolbarContents.values()].sort((a, b) => (a.order ?? 0) - (b.order ?? 0));\n },\n\n registerToolbarContent(content: ToolbarContentDefinition) {\n if (state.toolbarContents.has(content.id)) {\n warnDiagnostic(TOOLBAR_CONTENT_DUPLICATE, `Toolbar content \"${content.id}\" already registered`, grid.id);\n return;\n }\n state.toolbarContents.set(content.id, content);\n\n if (initialized || grid._hostElement.isConnected) {\n grid.refreshShellHeader?.();\n }\n },\n\n unregisterToolbarContent(contentId: string) {\n // Clean up\n const cleanup = state.toolbarContentCleanups.get(contentId);\n if (cleanup) {\n cleanup();\n state.toolbarContentCleanups.delete(contentId);\n }\n\n // Call onDestroy if defined\n const content = state.toolbarContents.get(contentId);\n if (content?.onDestroy) {\n content.onDestroy();\n }\n\n state.toolbarContents.delete(contentId);\n\n if (initialized) {\n grid.refreshShellHeader?.();\n }\n },\n };\n\n return controller;\n}\n\n/**\n * Update accordion section visual state.\n */\nfunction updateAccordionSectionState(renderRoot: Element, sectionId: string, expanded: boolean): void {\n const section = renderRoot.querySelector(`[data-section=\"${sectionId}\"]`);\n if (section) {\n section.classList.toggle('expanded', expanded);\n }\n}\n\n/**\n * Render content for a single accordion section.\n */\nfunction renderAccordionSectionContent(renderRoot: Element, state: ShellState, sectionId: string): void {\n const panel = state.toolPanels.get(sectionId);\n if (!panel?.render) return;\n\n const contentEl = renderRoot.querySelector(`[data-section=\"${sectionId}\"] .tbw-accordion-content`);\n if (!contentEl) return;\n\n const cleanup = panel.render(contentEl as HTMLElement);\n if (cleanup) {\n state.panelCleanups.set(sectionId, cleanup);\n }\n}\n// #endregion\n","/**\n * Shell Plugin (Class-based)\n *\n * Owns the grid shell: header bar, toolbar content, and tool panels.\n *\n * **Extraction #370 — Phase 1b (ownership inversion):** this plugin now owns\n * the shell **state** for the grid's lifetime (Approach A: state created once,\n * survives plugin re-inits, torn down on disconnect). Light-DOM parsing,\n * per-render work, and DOM construction move here in the following increments;\n * until each lands, core still drives that part via the resolved plugin\n * instance.\n *\n * @module Plugins/Shell\n */\n\nimport { HEADER_CONTENT_DUPLICATE, TOOL_PANEL_DUPLICATE, warnDiagnostic } from '../../core/internal/diagnostics';\nimport { BaseGridPlugin, type GridElement } from '../../core/plugin/base-plugin';\nimport type { GridConfig } from '../../core/types';\nimport {\n cleanupShellState,\n createShellState,\n parseLightDomShell,\n parseLightDomToolButtons,\n parseLightDomToolPanels,\n prepareForRerender,\n rebuildShellDOM,\n renderCustomToolbarContents,\n renderHeaderContent,\n renderPanelContent,\n setupClickOutsideDismiss,\n setupEscapeDismiss,\n setupShellEventListeners,\n setupToolPanelResize,\n shouldRenderShellHeader,\n updatePanelState,\n updateToolbarActiveStates,\n type ShellState,\n} from './shell';\nimport { createShellController, type ShellController, type ShellControllerGrid } from './shell-controller';\nimport styles from './shell.css?inline';\nimport './types';\nimport type {\n HeaderContentDefinition,\n OpenToolPanelOptions,\n ShellConfig,\n ToolbarContentDefinition,\n ToolPanelDefinition,\n} from './types';\n\n/**\n * Shell plugin for `<tbw-grid>`.\n *\n * @see {@link ShellConfig} for all configuration options.\n *\n * @internal Extends BaseGridPlugin.\n * @since 2.x (extraction #370)\n */\nexport class ShellPlugin extends BaseGridPlugin<ShellConfig> {\n /** @internal */\n readonly name = 'shell';\n /** @internal */\n override readonly styles = styles;\n\n // Extraction #370 — Approach A: the plugin owns the shell state for the\n // grid's lifetime. State is created once (via `ensureState`, lazily on first\n // access) and survives plugin re-inits (idempotent attach / non-destructive\n // detach); it is torn down only on grid disconnect via `disposeShellState()`.\n #state?: ShellState;\n #controller?: ShellController;\n\n // Document-level listeners the plugin owns for the shell's interactive\n // affordances. Each `#setupListeners` call cleans the previous registration\n // before re-adding (idempotent across re-wraps); torn down on disconnect.\n #resizeCleanup?: () => void;\n #clickOutsideCleanup?: () => void;\n #escapeDismissCleanup?: () => void;\n\n // The light-DOM shell-change handler (re-parses <tbw-grid-header> /\n // <tbw-grid-tool-buttons> / <tbw-grid-tool-panel> when the framework projects\n // them asynchronously). Registered in `attach` via the generic\n // `_registerLightDomHandler` seam; unregistered on disconnect.\n #shellChangeHandler?: () => void;\n\n // While a dropdown tool panel is open, this observer watches the render root\n // for the trigger being recreated by an outer framework (e.g. a React column\n // `headerRenderer` re-running on a column toggle) and re-pairs the popover's\n // anchor. MutationObserver callbacks run as a pre-paint microtask, so the\n // re-pair lands BEFORE the browser paints the dangling-anchor frame — no\n // flash. It also fires for every toggle (not a coalesced single rAF), so the\n // dropdown no longer intermittently disappears. Connected lazily while a\n // dropdown is open; disconnected on close and on grid disconnect.\n #reanchorObserver?: MutationObserver;\n #reanchorObserving = false;\n\n /** @internal Whether shell state has been created yet. */\n get hasState(): boolean {\n return this.#state !== undefined;\n }\n\n /**\n * @internal Create the canonical shell state + controller (idempotent).\n * The plugin owns shell-state construction; the controller binds to the grid\n * for UI updates. No-op once state exists, so it is safe to call on every\n * `attach` (and from a pre-connect API call), which keeps the shell self-\n * sufficient — core holds no shell-activation knowledge (extraction #370).\n */\n ensureState(grid: GridElement): void {\n if (this.#state) return;\n const state = createShellState();\n this.#state = state;\n this.#controller = createShellController(state, grid as ShellControllerGrid);\n }\n\n /** @internal Canonical shell state. */\n get shellState(): ShellState {\n return this.#state as ShellState;\n }\n\n /** @internal Shell controller. */\n get shellController(): ShellController {\n return this.#controller as ShellController;\n }\n\n // #region Public Shell API\n // The shell plugin's public, programmatic API. Obtain the instance via\n // `grid.getPluginByName('shell')`. These mirror the (now `@deprecated`)\n // `<tbw-grid>` delegate methods, which forward here for the v2.x window and\n // are removed in v3 (extraction #370). Each guards on `hasState` so a call\n // before the shell has initialized is a safe no-op.\n\n /** Whether the tool panel sidebar is currently open. */\n get isToolPanelOpen(): boolean {\n return this.hasState ? this.shellController.isPanelOpen : false;\n }\n\n /** IDs of the currently expanded accordion sections in the tool panel. */\n get expandedToolPanelSections(): string[] {\n return this.hasState ? this.shellController.expandedSections : [];\n }\n\n /**\n * Open the tool panel sidebar.\n * @param panelId - Optional section to expand on open (see {@link ShellController.openToolPanel}).\n * @param options - Optional open options. In `mode: 'dropdown'`, `options.anchor`\n * sets the element the popover anchors to (see {@link OpenToolPanelOptions}).\n */\n openToolPanel(panelId?: string, options?: OpenToolPanelOptions): void {\n this.shellController?.openToolPanel(panelId, options);\n }\n\n /** Close the tool panel sidebar. */\n closeToolPanel(): void {\n this.shellController?.closeToolPanel();\n }\n\n /**\n * Toggle the tool panel sidebar open or closed.\n * @param options - Optional open options forwarded when opening (e.g. `anchor`\n * for dropdown mode; see {@link OpenToolPanelOptions}).\n */\n toggleToolPanel(options?: OpenToolPanelOptions): void {\n this.shellController?.toggleToolPanel(options);\n }\n\n /** Toggle an accordion section expanded or collapsed within the tool panel. */\n toggleToolPanelSection(sectionId: string): void {\n this.shellController?.toggleToolPanelSection(sectionId);\n }\n\n /** Get all registered tool panel definitions. */\n getToolPanels(): ToolPanelDefinition[] {\n return this.hasState ? this.shellController.getToolPanels() : [];\n }\n\n /** Register a custom tool panel section. */\n registerToolPanel(panel: ToolPanelDefinition): void {\n if (!this.hasState) return;\n this.shellState.apiToolPanelIds.add(panel.id);\n this.shellController.registerToolPanel(panel);\n }\n\n /** Unregister a custom tool panel section. */\n unregisterToolPanel(panelId: string): void {\n if (!this.hasState) return;\n this.shellState.apiToolPanelIds.delete(panelId);\n this.shellController.unregisterToolPanel(panelId);\n }\n\n /** Get all registered header content definitions. */\n getHeaderContents(): HeaderContentDefinition[] {\n return this.hasState ? this.shellController.getHeaderContents() : [];\n }\n\n /** Register custom header content (rendered in the shell header bar). */\n registerHeaderContent(content: HeaderContentDefinition): void {\n if (!this.hasState) return;\n this.shellState.apiHeaderContentIds.add(content.id);\n this.shellController.registerHeaderContent(content);\n }\n\n /** Unregister custom header content. */\n unregisterHeaderContent(contentId: string): void {\n if (!this.hasState) return;\n this.shellState.apiHeaderContentIds.delete(contentId);\n this.shellController.unregisterHeaderContent(contentId);\n }\n\n /** Get all registered toolbar content definitions. */\n getToolbarContents(): ToolbarContentDefinition[] {\n return this.hasState ? this.shellController.getToolbarContents() : [];\n }\n\n /** Register custom toolbar content (rendered in the shell toolbar). */\n registerToolbarContent(content: ToolbarContentDefinition): void {\n this.shellController?.registerToolbarContent(content);\n }\n\n /** Unregister custom toolbar content. */\n unregisterToolbarContent(contentId: string): void {\n this.shellController?.unregisterToolbarContent(contentId);\n }\n // #endregion\n\n /** @internal */\n override attach(grid: GridElement): void {\n super.attach(grid);\n // Activate the shell on attach so an opted-in shell renders without core\n // ever calling into it (extraction #370 — core holds no shell-activation\n // knowledge). Idempotent: a pre-connect API call may have created state.\n this.ensureState(grid);\n // Register the light-DOM shell handlers via the generic seam. Core holds no\n // shell tag knowledge (extraction #370). `registerLightDomHandler` is keyed\n // by tag name, so re-attach simply overwrites — idempotent. Torn down on\n // grid disconnect in `disposeShellState` (Approach A: survives re-inits).\n const handler = (): void => this.syncLightDom();\n this.#shellChangeHandler = handler;\n grid._registerLightDomHandler('tbw-grid-header', handler);\n grid._registerLightDomHandler('tbw-grid-tool-buttons', handler);\n grid._registerLightDomHandler('tbw-grid-tool-panel', handler);\n }\n\n /** @internal Parse all light-DOM shell elements into the plugin's state. */\n #parseLightDom(grid: GridElement): void {\n const host = grid._hostElement;\n parseLightDomShell(host, this.shellState);\n parseLightDomToolButtons(host, this.shellState);\n parseLightDomToolPanels(host, this.shellState, grid._getToolPanelRendererFactory());\n }\n\n /**\n * @internal Generic hook: react to a light-DOM shell element being\n * added/removed/mutated (e.g. a framework projecting `<tbw-grid-header title>`\n * asynchronously). Re-parses, and if a title or tool-buttons container newly\n * appeared, re-merges the config (so `processConfig` folds it in) and re-wraps\n * the already-rendered shell header. No-op until the shell header is rendered.\n * Called by core from `refreshColumns` and the light-DOM observer.\n */\n override syncLightDom(): void {\n const grid = this.grid;\n if (!grid || !this.hasState) return;\n const state = this.shellState;\n const hadTitle = state.lightDomTitle;\n const hadToolButtons = state.hasToolButtonsContainer;\n this.#parseLightDom(grid);\n const hasTitle = state.lightDomTitle;\n const hasToolButtons = state.hasToolButtonsContainer;\n\n if ((hasTitle && !hadTitle) || (hasToolButtons && !hadToolButtons)) {\n grid._requestConfigMerge();\n // Re-wrap the shell header to reflect the new title / tool buttons.\n // `afterStructuralRender` rebuilds the chrome AND re-renders all shell\n // contents (header content, toolbar slots, panel), so no manual render is\n // needed here.\n if (grid._renderRoot.querySelector('.tbw-shell-header')) {\n prepareForRerender(state);\n this.afterStructuralRender();\n }\n }\n }\n\n // #region Generic structural hooks (extraction #370)\n // Core calls these via the plugin manager (parseLightDom /\n // beforeStructuralRender / needsStructuralRerender) — it holds no\n // shell-specific orchestration. The shell self-parses, self-prepares, and\n // self-decides re-render needs from its own state + DOM signature.\n\n /** @internal Generic hook: re-parse all light-DOM shell elements into state. */\n override parseLightDom(): void {\n const grid = this.grid;\n if (!grid || !this.hasState) return;\n this.#parseLightDom(grid);\n }\n\n /** @internal Generic hook: move toolbar buttons back ahead of a structural rebuild. */\n override beforeStructuralRender(): void {\n if (!this.hasState) return;\n prepareForRerender(this.shellState);\n }\n\n /**\n * @internal Generic hook: whether the shell's rendered DOM signature no longer\n * matches the new effective config (header appearing/disappearing, tool-panel\n * count/position/mode change) and therefore needs a structural re-render.\n * Reads the still-current (old) DOM vs the freshly merged config.\n */\n override needsStructuralRerender(): boolean {\n const grid = this.grid;\n if (!grid || !this.hasState) return false;\n const root = grid._renderRoot;\n const hadShell = !!root.querySelector('.has-shell');\n const hadToolPanel = !!root.querySelector('.tbw-tool-panel');\n const accordionSectionsBefore = root.querySelectorAll('.tbw-accordion-section').length;\n const prevPosition = (root.querySelector('.tbw-tool-panel') as HTMLElement | null)?.dataset.position ?? 'right';\n const prevMode = (root.querySelector('.tbw-shell-body') as HTMLElement | null)?.dataset.mode ?? 'overlay';\n\n const shell = grid.effectiveConfig?.shell;\n const nowNeedsShell = shouldRenderShellHeader(shell);\n const nowHasToolPanels = (shell?.toolPanels?.length ?? 0) > 0;\n const toolPanelCount = shell?.toolPanels?.length ?? 0;\n const newPosition = shell?.toolPanel?.position ?? 'right';\n const newMode = shell?.toolPanel?.mode ?? 'overlay';\n\n return (\n hadShell !== nowNeedsShell ||\n (!hadToolPanel && nowHasToolPanels) ||\n (hadToolPanel && toolPanelCount !== accordionSectionsBefore) ||\n (hadToolPanel && prevPosition !== newPosition) ||\n (hadToolPanel && prevMode !== newMode)\n );\n }\n // #endregion\n\n /**\n * @internal Fold the plugin's shell state into the effective config.\n *\n * Called by core's config merge ({@link ConfigManager.merge}) via the neutral\n * `processConfig` plugin hook \\u2014 core holds no shell-config knowledge. Mutates\n * `config.shell` in place from light-DOM-parsed + API-registered shell state\n * (title, header content, tool buttons, tool panels, header/toolbar contents).\n *\n * Extraction #370, Task 1b \\u2014 config ownership inversion: replaces core's old\n * `ConfigManager.#mergeShellConfig`.\n */\n /**\n * @internal Self-collect plugin tool panels + header contents into shell\n * state. Clears stale plugin-contributed entries (preserving light-DOM +\n * API-registered) then re-collects fresh from all attached plugins via the\n * neutral `grid._pluginShellContributions()` seam. Idempotent — safe to run on\n * every config merge, which is how plugin re-inits are picked up.\n *\n * Extraction #370: replaces core's `#collectPluginShellContributions` plus the\n * plugin-reinit cleanup block in core's `#updatePluginConfigs`.\n */\n #syncPluginContributions(grid: GridElement): void {\n const state = this.shellState;\n const gridId = grid._hostElement.id;\n\n // Drop stale plugin-contributed tool panels (keep light-DOM + API panels).\n for (const panelId of [...state.toolPanels.keys()]) {\n if (state.lightDomToolPanelIds.has(panelId) || state.apiToolPanelIds.has(panelId)) continue;\n state.panelCleanups.get(panelId)?.();\n state.panelCleanups.delete(panelId);\n state.toolPanels.delete(panelId);\n }\n // Drop stale plugin-contributed header contents (keep API-registered).\n for (const contentId of [...state.headerContents.keys()]) {\n if (state.apiHeaderContentIds.has(contentId)) continue;\n state.headerContentCleanups.get(contentId)?.();\n state.headerContentCleanups.delete(contentId);\n state.headerContents.delete(contentId);\n }\n\n const { toolPanels, headerContents } = grid._pluginShellContributions();\n\n for (const panel of toolPanels) {\n // Precedence (#370): an API-registered panel wins over a plugin; a plugin\n // wins over a colliding light-DOM panel (the plugin owns its own DOM, so\n // it must own the id).\n if (state.apiToolPanelIds.has(panel.id)) continue;\n if (state.lightDomToolPanelIds.has(panel.id)) {\n warnDiagnostic(\n TOOL_PANEL_DUPLICATE,\n `Tool panel \"${panel.id}\" is provided by a plugin; ignoring the matching light-DOM <tbw-grid-tool-panel>.`,\n gridId,\n );\n state.panelCleanups.get(panel.id)?.();\n state.panelCleanups.delete(panel.id);\n state.lightDomToolPanelIds.delete(panel.id);\n state.adapterBoundToolPanelIds.delete(panel.id);\n }\n state.toolPanels.set(panel.id, panel);\n }\n\n for (const content of headerContents) {\n if (state.apiHeaderContentIds.has(content.id)) continue;\n if (state.headerContents.has(content.id)) {\n warnDiagnostic(\n HEADER_CONTENT_DUPLICATE,\n `Header content \"${content.id}\" is provided by a plugin; ignoring the matching light-DOM definition.`,\n gridId,\n );\n state.headerContentCleanups.get(content.id)?.();\n state.headerContentCleanups.delete(content.id);\n }\n state.headerContents.set(content.id, content);\n }\n }\n\n override processConfig(config: GridConfig): void {\n if (!this.hasState) return;\n const state = this.shellState;\n\n // Self-parse light-DOM shell elements + self-collect plugin shell\n // contributions before folding state into config (extraction #370 —\n // replaces core's `#parseLightDom` + `#collectPluginShellContributions`).\n const grid = this.grid;\n if (grid) {\n this.#parseLightDom(grid);\n this.#syncPluginContributions(grid);\n }\n\n // Constructor-supplied shell config is the base layer: the canonical\n // `features: { shell: ... }` opt-in passes the config value here (and an\n // explicit `plugins: [new ShellPlugin(cfg)]` does too). An explicit\n // top-level `gridConfig.shell` overrides it; parsed light-DOM / API state\n // folds on top below. Empty for the auto-registered instance\n // (`new ShellPlugin()`), so existing default-on behavior is unchanged.\n const ctor = this.resolvedConfig;\n\n // Clone shell hierarchy to avoid mutating the original gridConfig.\n config.shell = { ...ctor, ...config.shell };\n config.shell.header = { ...ctor.header, ...config.shell.header };\n\n // Light DOM title (does not override an explicit config title).\n if (state.lightDomTitle && !config.shell.header.title) {\n config.shell.header.title = state.lightDomTitle;\n }\n\n // Light DOM header content elements.\n if (state.lightDomHeaderContent?.length > 0) {\n config.shell.header.lightDomContent = state.lightDomHeaderContent;\n }\n\n // Tool-buttons container presence.\n if (state.hasToolButtonsContainer) {\n config.shell.header.hasToolButtonsContainer = true;\n }\n\n // Tool panels (from plugins + API + Light DOM), sorted by order.\n if (state.toolPanels.size > 0) {\n const panels = Array.from(state.toolPanels.values());\n panels.sort((a, b) => (a.order ?? 100) - (b.order ?? 100));\n config.shell.toolPanels = panels;\n }\n\n // Header contents (from plugins + API), sorted by order.\n if (state.headerContents.size > 0) {\n const contents = Array.from(state.headerContents.values());\n contents.sort((a, b) => (a.order ?? 100) - (b.order ?? 100));\n config.shell.headerContents = contents;\n }\n\n // Toolbar contents: merge ORIGINAL config contents (from the source\n // gridConfig, not a previous merge) with API-registered contents so stale\n // API entries never accumulate. Config takes precedence by id.\n const apiContents = Array.from(state.toolbarContents.values());\n const originalConfigContents = this.grid?._sourceConfig?.shell?.header?.toolbarContents ?? [];\n const configIds = new Set(originalConfigContents.map((c) => c.id));\n const mergedContents = [...originalConfigContents];\n for (const content of apiContents) {\n if (!configIds.has(content.id)) {\n mergedContents.push(content);\n }\n }\n mergedContents.sort((a, b) => (a.order ?? 0) - (b.order ?? 0));\n config.shell.header.toolbarContents = mergedContents;\n }\n\n /**\n * @internal Wrap the freshly built (bare) grid DOM in shell chrome.\n *\n * Fired synchronously by core at the end of every structural (re)build, before\n * paint. Core builds a BARE grid; this hook relocates the existing\n * `.tbw-grid-content` node into shell chrome (header bar, toolbar, tool panel)\n * via {@link rebuildShellDOM}, which preserves the content subtree (and all its\n * listeners / cached refs) intact. No-op when no shell is configured.\n *\n * Extraction #370, Task 1b — DOM ownership inversion: the shell DOM is now\n * constructed by the plugin, not core.\n */\n override afterStructuralRender(): void {\n const grid = this.grid;\n if (!grid || !this.hasState) return;\n const state = this.shellState;\n const hasShell = rebuildShellDOM(\n grid._renderRoot,\n grid.effectiveConfig?.shell,\n { isPanelOpen: state.isPanelOpen, expandedSections: state.expandedSections },\n grid.effectiveConfig?.icons,\n );\n if (hasShell) {\n this.#setupListeners(grid);\n this.shellController.setInitialized(true);\n this.#renderShellContents(grid);\n }\n }\n\n /**\n * @internal Re-anchor an open dropdown tool panel after a NON-structural\n * render.\n *\n * A column-visibility toggle (or reorder) re-renders the grid header WITHOUT\n * a full structural rebuild — it fires `afterRender`, not\n * `afterStructuralRender`. When the column-chooser trigger lives in the grid\n * header (e.g. a custom `[data-panel-toggle]` button in a column\n * `headerRenderer`), that re-render recreates the trigger node, so it loses\n * the `anchor-name` that paired it with the open popover. The popover then\n * collapses to the viewport / grid corner.\n *\n * Two cooperating mechanisms keep the popover anchored with NO flash:\n * 1. A synchronous {@link ShellController.reanchorOpenDropdown} here catches\n * the case where the trigger was recreated synchronously during this\n * render (in-place re-pair, before paint).\n * 2. A {@link MutationObserver} (started here, kept running while the panel is\n * open) catches the case where an outer framework commits the replacement\n * trigger on a LATER task. Its callback runs as a pre-paint microtask, so\n * the re-pair lands before the dangling-anchor frame is painted.\n *\n * `reanchorOpenDropdown()` is idempotent and cheap (early-returns unless a\n * dropdown is open with a broken anchor), so running it on every render and\n * every mutation is safe.\n */\n override afterRender(): void {\n const grid = this.grid;\n if (!grid || !this.hasState) return;\n const state = this.shellState;\n\n // In-place shell header title sync (extraction #370 — replaces core's\n // `#updateShellHeaderInPlace`). Idempotent: only touches the existing\n // `.tbw-shell-header`, no-op when no shell is rendered.\n this.#syncShellHeaderTitle(grid);\n\n // Re-render any open tool-panel sections whose content was cleared during a\n // plugin re-init (extraction #370 — replaces core's `_renderOpenPanelContent`).\n // `renderPanelContent` is idempotent — it only fills empty sections.\n if (state.isPanelOpen) {\n renderPanelContent(grid._renderRoot, state);\n }\n\n // Dropdown re-anchor after a NON-structural render (see method docs below).\n if (!state.isPanelOpen) {\n this.#stopReanchorObserver();\n return;\n }\n this.shellController.reanchorOpenDropdown();\n this.#startReanchorObserver(grid);\n }\n\n /**\n * @internal Sync the shell header title element in place from the effective\n * config (or parsed light-DOM title) without a full re-render. Extracted from\n * core's former `#updateShellHeaderInPlace` (#370).\n */\n #syncShellHeaderTitle(grid: GridElement): void {\n const shellHeader = grid._renderRoot.querySelector('.tbw-shell-header');\n if (!shellHeader) return;\n\n const title = grid.effectiveConfig?.shell?.header?.title ?? this.shellState.lightDomTitle;\n\n let titleEl = shellHeader.querySelector('.tbw-shell-title') as HTMLElement | null;\n if (title) {\n if (!titleEl) {\n titleEl = document.createElement('h2');\n titleEl.className = 'tbw-shell-title';\n titleEl.setAttribute('part', 'shell-title');\n shellHeader.insertBefore(titleEl, shellHeader.firstChild);\n }\n titleEl.textContent = title;\n } else if (titleEl) {\n titleEl.remove();\n }\n }\n\n /** @internal Start (idempotently) the dropdown re-anchor observer. */\n #startReanchorObserver(grid: GridElement): void {\n // Only dropdown mode needs the observer. If the mode changed away from\n // 'dropdown' while the panel is still open, tear down any observer started\n // by an earlier render instead of leaving it watching the render root.\n if (grid.effectiveConfig?.shell?.toolPanel?.mode !== 'dropdown') {\n this.#stopReanchorObserver();\n return;\n }\n if (typeof MutationObserver === 'undefined') return;\n if (!this.#reanchorObserver) {\n this.#reanchorObserver = new MutationObserver(() => {\n if (!this.grid || !this.hasState || !this.shellState.isPanelOpen) {\n this.#stopReanchorObserver();\n return;\n }\n this.shellController.reanchorOpenDropdown();\n });\n }\n if (!this.#reanchorObserving) {\n this.#reanchorObserver.observe(grid._renderRoot, { childList: true, subtree: true });\n this.#reanchorObserving = true;\n }\n }\n\n /** @internal Stop the dropdown re-anchor observer if running. */\n #stopReanchorObserver(): void {\n if (this.#reanchorObserver && this.#reanchorObserving) {\n this.#reanchorObserver.disconnect();\n this.#reanchorObserving = false;\n }\n }\n\n /**\n * @internal Render all shell contents into the freshly built shell chrome.\n *\n * Runs from {@link afterStructuralRender} after the chrome is wrapped and\n * listeners are attached. Renders plugin header content + custom toolbar\n * slots, applies the configured default-open / legacy open-on-load behavior,\n * and restores an already-open panel's content. All render functions operate\n * on the shell DOM (queried from `_renderRoot`), not on the grid-body refs,\n * so they are safe to run before `#afterConnect` caches those.\n *\n * Extraction #370, Task 1b (Step 1b.1.3) — replaces core's per-render shell\n * content block in `#afterConnect` / `#afterShellRefresh`.\n */\n #renderShellContents(grid: GridElement): void {\n const state = this.shellState;\n const renderRoot = grid._renderRoot;\n const shellConfig = grid.effectiveConfig?.shell;\n\n // Render plugin header content + custom toolbar contents (render modes).\n renderHeaderContent(renderRoot, state);\n renderCustomToolbarContents(renderRoot, shellConfig, state);\n\n // Pre-expand the configured default section.\n const toolPanelCfg = shellConfig?.toolPanel;\n const defaultOpen = toolPanelCfg?.defaultOpen;\n if (defaultOpen && state.toolPanels.has(defaultOpen)) {\n state.expandedSections.add(defaultOpen);\n }\n\n // Decide whether the sidebar should be open on load.\n // NOTE: In v2.x `defaultOpen` alone also opens the sidebar (legacy\n // behavior). v3.0.0 will drop that — see issue #259 and the\n // @deprecated note on ToolPanelConfig.defaultOpen.\n // Search marker: TOOLPANEL-OPEN-LEGACY-259\n const shouldOpenOnLoad =\n toolPanelCfg?.locked === true ||\n toolPanelCfg?.initialState === 'open' ||\n // Legacy v2 path — remove in v3.0.0 (#259)\n (toolPanelCfg?.initialState === undefined && defaultOpen !== undefined && state.toolPanels.has(defaultOpen));\n if (!state.isPanelOpen && state.toolPanels.size > 0 && shouldOpenOnLoad) {\n this.openToolPanel();\n }\n\n // Restore panel content if the panel was already open (e.g. after a position\n // change re-render or a plugin re-init that triggered a shell refresh).\n if (state.isPanelOpen) {\n updatePanelState(renderRoot, state);\n renderPanelContent(renderRoot, state, {\n expand: grid.effectiveConfig?.icons?.expand,\n collapse: grid.effectiveConfig?.icons?.collapse,\n });\n updateToolbarActiveStates(renderRoot, state);\n // In dropdown mode `rebuildShellDOM` created a fresh `.tbw-tool-panel`, so\n // the popover must be re-shown in the top layer and re-anchored — the\n // trigger may have been recreated by the same re-render (e.g. a custom\n // column-header button after a column toggle/reorder). Otherwise the\n // popover loses its anchor and collapses to the viewport corner.\n this.shellController.reanchorOpenDropdown();\n }\n }\n\n /**\n * @internal Attach shell event + dismiss/resize listeners.\n *\n * Cleans any prior document-level registrations before re-adding, so it is\n * safe to call on every re-wrap (initial render, shell refresh, plugin\n * re-init) without leaking or double-attaching listeners.\n */\n #setupListeners(grid: GridElement): void {\n const renderRoot = grid._renderRoot;\n const shellConfig = grid.effectiveConfig?.shell;\n const state = this.shellState;\n\n setupShellEventListeners(renderRoot, shellConfig, state, {\n onPanelToggle: () => this.toggleToolPanel(),\n onSectionToggle: (sectionId: string) => this.toggleToolPanelSection(sectionId),\n onPanelClose: () => this.closeToolPanel(),\n });\n\n // Tool panel resize handle — persists the new width as a CSS variable on\n // the host element.\n this.#resizeCleanup?.();\n this.#resizeCleanup = setupToolPanelResize(renderRoot, shellConfig, (width: number) => {\n grid._hostElement.style.setProperty('--tbw-tool-panel-width', `${width}px`);\n });\n\n // Click-outside dismiss for an open overlay tool panel.\n this.#clickOutsideCleanup?.();\n this.#clickOutsideCleanup = setupClickOutsideDismiss(grid._hostElement, shellConfig, state, () =>\n this.closeToolPanel(),\n );\n\n // Escape-to-close dismiss for overlay tool panels.\n this.#escapeDismissCleanup?.();\n this.#escapeDismissCleanup = setupEscapeDismiss(shellConfig, state, () => this.closeToolPanel());\n }\n\n /** @internal Remove the document-level shell listeners. */\n #teardownListeners(): void {\n this.#resizeCleanup?.();\n this.#resizeCleanup = undefined;\n this.#clickOutsideCleanup?.();\n this.#clickOutsideCleanup = undefined;\n this.#escapeDismissCleanup?.();\n this.#escapeDismissCleanup = undefined;\n }\n\n /** @internal */\n override detach(): void {\n // Non-destructive detach (Approach A): shell state and document listeners\n // survive plugin re-inits. `#setupListeners` cleans-before-add on the next\n // re-wrap, so there is nothing to tear down here. Real teardown happens in\n // `disposeShellState()` on grid disconnect.\n super.detach();\n }\n\n /**\n * @internal Tear down shell state on grid disconnect (Approach A).\n * Idempotent and harmless when no state exists.\n */\n disposeShellState(): void {\n this.#teardownListeners();\n this.#stopReanchorObserver();\n this.#reanchorObserver = undefined;\n // Unregister the light-DOM shell handlers (registered in `attach`). `detach`\n // does not clear `this.grid`, so the reference is still valid here.\n const grid = this.grid as GridElement | undefined;\n if (grid && this.#shellChangeHandler) {\n grid._unregisterLightDomHandler('tbw-grid-header');\n grid._unregisterLightDomHandler('tbw-grid-tool-buttons');\n grid._unregisterLightDomHandler('tbw-grid-tool-panel');\n this.#shellChangeHandler = undefined;\n }\n if (this.#state) {\n cleanupShellState(this.#state);\n this.#controller?.setInitialized(false);\n }\n }\n}\n"],"names":["gridPrefix","gridId","pluginName","TOOL_PANEL_NOT_FOUND","TOOL_PANEL_DUPLICATE","HEADER_CONTENT_DUPLICATE","formatDiagnostic","code","message","toLowerCase","docsUrl","warnDiagnostic","console","warn","_F","split","map","s","reverse","join","RegExp","slice","DANGEROUS_TAGS","Set","DANGEROUS_ATTR_PATTERN","URL_ATTRS","DANGEROUS_URL_PROTOCOL","sanitizeHTML","html","indexOf","template","document","createElement","innerHTML","root","toRemove","elements","querySelectorAll","el","tagName","has","push","namespaceURI","Array","from","attributes","some","attr","test","name","attrsToRemove","attrName","value","forEach","removeAttribute","remove","sanitizeNode","content","DEFAULT_GRID_ICONS","expand","collapse","sortAsc","sortDesc","sortNone","submenuArrow","dragHandle","toolPanel","filter","filterActive","print","BaseGridPlugin","static","aliases","version","styles","cellRenderers","headerRenderers","cellEditors","grid","config","userConfig","abortController","defaultConfig","constructor","this","resolvedConfig","mergeConfigsFrom","others","length","merged","sources","key","Object","keys","other","otherConfig","entries","ownerA","ownerB","Error","assign","refreshUserConfigFrom","target","source","attach","abort","AbortController","detach","getPlugin","PluginClass","emit","eventName","detail","dispatchEvent","CustomEvent","bubbles","emitCancelable","event","cancelable","defaultPrevented","on","eventType","callback","_pluginManager","subscribe","off","unsubscribe","emitPluginEvent","broadcast","requestRender","requestColumnsRender","requestRenderWithFocus","requestAfterRender","requestVirtualRefresh","rows","sourceRows","columns","visibleColumns","_visibleColumns","gridElement","_hostElement","disconnectSignal","signal","gridIcons","userIcons","gridConfig","icons","isAnimationEnabled","mode","effectiveConfig","animation","host","getComputedStyle","getPropertyValue","trim","animationDuration","durationStr","parsed","parseInt","isNaN","setIcon","element","iconKey","pluginOverride","dataset","icon","replace","expanded","jsIcon","resolveJsIcon","HTMLElement","appendChild","cloneNode","updateSortIndicator","cell","direction","querySelector","indicator","setAttribute","className","insertBefore","codeOrMessage","id","throwDiagnostic","GridClasses","applyAttrs","attrs","tag","children","div","button","type","gridContentTemplate","cloneGridContent","buildGridDOM","options","fragment","createDocumentFragment","hasShell","shellBody","shellHeader","contentWrapper","buildShellHeader","header","part","role","title","titleEl","textContent","toolbar","btn","configButtons","hasRender","apiButtons","hasCustomContent","b","showToggle","hasPanels","toggleBtn","isPanelOpen","String","toolPanelIcon","buildShellBody","body","hasPanel","panels","isSinglePanel","gridContent","panelEl","class","position","popover","resizeHandlePosition","makeCloseButton","inline","closeBtn","inlineClose","showCloseButton","panelHeader","panelContent","accordion","panel","section","isExpanded","headerBtn","iconSpan","titleSpan","chevronSpan","expandIcon","headerRow","iconToString","outerHTML","shouldRenderShellHeader","toolbarContents","toolPanels","headerContents","lightDomContent","hasToolButtonsContainer","shouldRenderHeaderBar","visible","isInGrid","node","contains","getRootNode","ShadowRoot","renderHeaderContent","renderRoot","state","hasLightDomContent","lightDomHeaderContent","lightDomContentMoved","hasPluginContent","size","contentArea","style","display","sortedContents","values","sort","a","order","existingCleanup","headerContentCleanups","get","delete","container","cleanup","render","set","renderPanelContent","_icons","panelId","expandedSections","classList","toggle","panelCleanups","updateToolbarActiveStates","panelToggle","updatePanelState","width","DROPDOWN_ANCHOR_NAME","dropdownAnchorSeq","supportsAnchorPositioning","CSS","supports","showToolPanelDropdown","anchorEl","corner","dropdownAnchorEl","anchor","hasAttribute","showPopover","anchorName","dropdownAnchorName","setProperty","removeProperty","rect","getBoundingClientRect","getAttribute","top","Math","round","bottom","left","right","viewportWidth","window","innerWidth","positionDropdownFallback","prepareForRerender","toolbarContentCleanups","clear","buildShellOptions","shellConfig","runtimeState","collapseIcon","sortedPanels","headerOptions","toolPanelToggle","locked","c","hasElement","bodyOptions","p","rebuildShellDOM","existingRoot","existingContent","preservedNodes","n","firstChild","nextSibling","nodeType","replaceChildren","buildGridDOMIntoElement","freshContent","replaceWith","createShellController","initialized","controller","isInitialized","setInitialized","openToolPanel","targetSectionId","toggleToolPanelSection","add","defaultOpenId","shell","defaultOpen","firstPanel","shadow","_renderRoot","_accordionIcons","explicit","resolveDropdownAnchor","_emit","sections","closeToolPanel","onClose","hidePopover","hideToolPanelDropdown","toggleToolPanel","reanchorOpenDropdown","stored","isConnected","fresh","trigger","prev","repairDropdownAnchor","sectionId","updateAccordionSectionState","otherId","otherPanel","contentEl","renderAccordionSectionContent","getToolPanels","registerToolPanel","refreshShellHeader","unregisterToolPanel","getHeaderContents","registerHeaderContent","unregisterHeaderContent","contentId","onDestroy","getToolbarContents","registerToolbarContent","unregisterToolbarContent","ShellPlugin","resizeCleanup","clickOutsideCleanup","escapeDismissCleanup","shellChangeHandler","reanchorObserver","reanchorObserving","hasState","ensureState","Map","lightDomTitle","lightDomToolPanelIds","adapterBoundToolPanelIds","lightDomToolbarContentIds","apiToolPanelIds","apiHeaderContentIds","shellState","shellController","isToolPanelOpen","expandedToolPanelSections","super","handler","syncLightDom","_registerLightDomHandler","parseLightDom","headerEl","parseLightDomShell","toolButtonsContainer","contentDef","parseLightDomToolButtons","rendererFactory","tooltip","adapterRenderer","wrapper","existingPanel","firstAdapterAttach","attrsChanged","parseLightDomToolPanels","_getToolPanelRendererFactory","hadTitle","hadToolButtons","hasTitle","hasToolButtons","_requestConfigMerge","afterStructuralRender","beforeStructuralRender","needsStructuralRerender","hadShell","hadToolPanel","accordionSectionsBefore","prevPosition","prevMode","syncPluginContributions","_pluginShellContributions","processConfig","ctor","contents","apiContents","originalConfigContents","_sourceConfig","configIds","mergedContents","setupListeners","renderShellContents","afterRender","syncShellHeaderTitle","startReanchorObserver","stopReanchorObserver","MutationObserver","observe","childList","subtree","disconnect","configContents","stateContents","allContents","slot","renderCustomToolbarContents","toolPanelCfg","shouldOpenOnLoad","initialState","callbacks","addEventListener","e","closest","onPanelToggle","onPanelClose","onSectionToggle","setupShellEventListeners","onResize","handle","startX","startWidth","maxWidth","isResizing","onMouseMove","preventDefault","delta","clientX","newWidth","min","max","onMouseUp","transition","cursor","userSelect","finalWidth","removeEventListener","onMouseDown","setupToolPanelResize","closeOnClickOutside","composedPath","Element","matches","setupClickOutsideDismiss","setupEscapeDismiss","teardownListeners","disposeShellState","_unregisterLightDomHandler","cleanupShellState"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sources":["../../../../../../libs/grid/src/lib/core/internal/diagnostics.ts","../../../../../../libs/grid/src/lib/core/internal/sanitize.ts","../../../../../../libs/grid/src/lib/core/types.ts","../../../../../../libs/grid/src/lib/core/plugin/base-plugin.ts","../../../../../../libs/grid/src/lib/core/constants.ts","../../../../../../libs/grid/src/lib/core/internal/dom-builder.ts","../../../../../../libs/grid/src/lib/plugins/shell/shell.ts","../../../../../../libs/grid/src/lib/plugins/shell/shell-controller.ts","../../../../../../libs/grid/src/lib/plugins/shell/ShellPlugin.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, deprecation)\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 */\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 or title. */\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","// Centralized template expression evaluation & sanitization utilities.\n// Responsible for safely interpolating {{ }} expressions while blocking\n// access to dangerous globals / reflective capabilities.\nimport type { CompiledViewFunction, EvalContext } from '../types';\n\n// #region Constants\nconst EXPR_RE = /{{\\s*([^}]+)\\s*}}/g;\nconst EMPTY_SENTINEL = '__DG_EMPTY__';\nconst SAFE_EXPR = /^[\\w$. '?+\\-*/%:()!<>=,&|]+$/;\n\n// Forbidden identifiers built at runtime to avoid static analysis scanners\n// from flagging string literals (e.g. socket.dev \"network access\" / \"uses eval\").\n// Stored as reversed, pipe-delimited tokens — reversed back when constructing the regex.\nconst _F =\n '__otorp__|__retteGenifed__|__retteSenifed__|rotcurtsnoc|wodniw|sihTlabolg|labolg|ssecorp|noitcnuF|tropmi|lave|tcelfeR|yxorP|rorrE|stnemugra|tnemucod|noitacol|eikooc|egarotSlacol|egarotSnoisses|BDdexedni|hctef|tseuqeRpttHLMX|tekcoSbeW|rekroW|rekroWderahS|rekroWecivreS|renepo|tnerap|pot|semarf|fles'\n .split('|')\n .map((s) => s.split('').reverse().join(''));\nconst FORBIDDEN = new RegExp(`__(proto|defineGetter|defineSetter)|${_F.slice(3).join('|')}|this\\\\b`);\n// #endregion\n\n// #region HTML Sanitization\n\n/**\n * Escape a plain text string for safe insertion into HTML.\n * Converts special HTML characters to their entity equivalents.\n *\n * @param text - Plain text string to escape\n * @returns HTML-safe string\n */\nexport function escapeHtml(text: string): string {\n if (!text || typeof text !== 'string') return '';\n return text\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;')\n .replace(/'/g, '&#39;');\n}\n\n/**\n * Tags that are considered dangerous and will be completely removed.\n * These can execute scripts, load external resources, or manipulate the page.\n */\nconst DANGEROUS_TAGS = new Set(\n 'script|iframe|object|embed|form|input|button|textarea|select|link|meta|base|style|template|slot|portal|frame|frameset|applet|noscript|noembed|plaintext|xmp|listing'.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 * This is a defense-in-depth measure for content rendered via innerHTML.\n *\n * **When to use:** ANY path that writes a user-supplied string to `innerHTML`\n * MUST pass through this function — `gridConfig.icons.*` (icon strings can be\n * SVG markup), cell renderer string returns, group-header renderer string\n * returns, light-DOM tool panel fallback content, plugin wrappers, and any new\n * extension point that turns a user string into markup. Using `textContent` is\n * wrong here (it renders HTML/SVG icon strings as literal text); using raw\n * `innerHTML` is wrong (it is an XSS sink). The canonical reference\n * implementation is `core/internal/rows.ts` (cell renderer path) — mirror it.\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 * 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 // 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 }\n | { type: 'str'; v: string }\n | { type: 'id'; v: string }\n | { 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 return 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","import type { ToolPanelDefinition } from '../plugins/shell/types';\nimport type { RenderPhase } from './internal/render-scheduler';\nimport type { RowPosition, ScrollMapping } from './internal/virtualization';\nimport type { AfterCellRenderContext, AfterRowRenderContext, CellMouseEvent } from './plugin/types';\n\n/**\n * Position entry for a single row in the position cache.\n * Part of variable row height virtualization.\n *\n * Re-exported from position-cache.ts for public API consistency.\n *\n * @see VirtualState.positionCache\n * @category Plugin Development\n */\nexport type RowPositionEntry = RowPosition;\n\n// #region ScrollToRowOptions\n/**\n * Options for the {@link PublicGrid.scrollToRow} method.\n *\n * @group Focus & Navigation\n *\n * @example\n * ```typescript\n * grid.scrollToRow(42, { align: 'center', behavior: 'smooth' });\n * ```\n * @since 1.23.0\n */\nexport interface ScrollToRowOptions {\n /**\n * Where to position the row in the viewport.\n *\n * - `'start'` — top of the viewport\n * - `'center'` — vertically centered\n * - `'end'` — bottom of the viewport\n * - `'nearest'` — scroll only if the row is outside the viewport (default)\n *\n * @defaultValue `'nearest'`\n */\n align?: 'start' | 'center' | 'end' | 'nearest';\n /**\n * Scroll behavior.\n *\n * - `'instant'` — jump immediately (default)\n * - `'smooth'` — animate the scroll\n *\n * @defaultValue `'instant'`\n */\n behavior?: 'smooth' | 'instant';\n}\n// #endregion\n\n// #region PublicGrid Interface\n/**\n * Public API interface for DataGrid component.\n *\n * **Property Getters vs Setters:**\n *\n * Property getters return the EFFECTIVE (resolved) value after merging all config sources.\n * This is the \"current situation\" - what consumers and plugins need to know.\n *\n * Property setters accept input values which are merged into the effective config.\n * Multiple sources can contribute (gridConfig, columns prop, light DOM, individual props).\n *\n * For example:\n * - `grid.fitMode` returns the resolved fitMode (e.g., 'stretch' even if you set undefined)\n * - `grid.columns` returns the effective columns after merging\n * - `grid.gridConfig` returns the full effective config\n * @since 0.1.1\n */\nexport interface PublicGrid<T = any> {\n /**\n * Full config object. Setter merges with other inputs per precedence rules.\n * Getter returns the effective (resolved) config.\n */\n gridConfig?: GridConfig<T>;\n /**\n * Column definitions.\n * Getter returns effective columns (after merging config, light DOM, inference).\n */\n columns?: ColumnConfig<T>[];\n /** Current row data (after plugin processing like grouping, filtering). */\n rows?: T[];\n /** Insert a row at a visible index, bypassing the sort/filter pipeline. Auto-animates by default. */\n insertRow?(index: number, row: T, animate?: boolean): Promise<void>;\n /** Remove a row at a visible index, bypassing the sort/filter pipeline. Auto-animates by default. */\n removeRow?(index: number, animate?: boolean): Promise<T | undefined>;\n /** Apply a batch of add/update/remove mutations in a single render cycle. */\n applyTransaction?(transaction: RowTransaction<T>, animate?: boolean): Promise<TransactionResult<T>>;\n /** Batch-friendly version — merges rapid calls within a single animation frame. */\n applyTransactionAsync?(transaction: RowTransaction<T>): Promise<TransactionResult<T>>;\n /** Resolves once the component has finished initial work (layout, inference). */\n ready?: () => Promise<void>;\n /** Force a layout / measurement pass (e.g. after container resize). */\n forceLayout?: () => Promise<void>;\n /** Return effective resolved config (after inference & precedence). */\n getConfig?: () => Promise<Readonly<GridConfig<T>>>;\n /** Toggle expansion state of a group row by its generated key. */\n toggleGroup?: (key: string) => Promise<void>;\n\n // Custom Styles API\n /**\n * Register custom CSS styles to be injected into the grid.\n * Use this to style custom cell renderers, editors, or detail panels.\n * @param id - Unique identifier for the style block (for removal/updates)\n * @param css - CSS string to inject\n */\n registerStyles?: (id: string, css: string) => void;\n /**\n * Remove previously registered custom styles.\n * @param id - The ID used when registering the styles\n */\n unregisterStyles?: (id: string) => void;\n /**\n * Get list of registered custom style IDs.\n */\n getRegisteredStyles?: () => string[];\n\n // Plugin API\n /**\n * Get a plugin instance by its class.\n *\n * **Prefer {@link getPluginByName}** — it avoids importing the plugin class\n * and returns the actual registered instance with full type narrowing.\n *\n * @example\n * ```typescript\n * // Preferred: by name\n * const selection = grid.getPluginByName('selection');\n *\n * // Alternative: by class\n * const selection = grid.getPlugin(SelectionPlugin);\n * if (selection) {\n * selection.selectAll();\n * }\n * ```\n */\n getPlugin?<P>(PluginClass: new (...args: any[]) => P): P | undefined;\n /**\n * Get a plugin instance by its name.\n *\n * When a plugin augments the `PluginNameMap` interface, the return\n * type is narrowed automatically:\n *\n * ```typescript\n * const editing = grid.getPluginByName('editing');\n * editing?.beginBulkEdit(0); // ✅ typed as EditingPlugin\n * ```\n *\n * For unknown names the return type falls back to `GridPlugin | undefined`.\n */\n getPluginByName?<K extends string>(\n name: K,\n ): (K extends keyof PluginNameMap ? PluginNameMap[K] : GridPlugin) | undefined;\n\n // Shell API\n /**\n * Re-render the shell header (title, column groups, toolbar).\n * Call this after dynamically adding/removing tool panels or toolbar buttons.\n */\n refreshShellHeader?(): void;\n /**\n * Register a custom tool panel in the sidebar.\n *\n * @example\n * ```typescript\n * grid.registerToolPanel({\n * id: 'analytics',\n * title: 'Analytics',\n * icon: '📊',\n * render: (container) => {\n * container.innerHTML = '<div>Charts here...</div>';\n * }\n * });\n * ```\n */\n registerToolPanel?(panel: ToolPanelDefinition): void;\n /**\n * Unregister a previously registered tool panel.\n */\n unregisterToolPanel?(panelId: string): void;\n /**\n * Open the tool panel sidebar.\n * @param panelId - Optional ID of the section to expand on open. Takes precedence\n * over `shell.toolPanel.defaultOpen`. Falls back to default behavior with a\n * warning if the ID is not registered.\n */\n openToolPanel?(panelId?: string): void;\n /**\n * Close the tool panel sidebar.\n */\n closeToolPanel?(): void;\n /**\n * Toggle the tool panel sidebar open or closed.\n */\n toggleToolPanel?(): void;\n /**\n * Toggle an accordion section expanded or collapsed within the tool panel.\n * @param sectionId - The ID of the section to toggle\n */\n toggleToolPanelSection?(sectionId: string): void;\n\n // State Persistence API\n /**\n * Get the current column state including order, width, visibility, and sort.\n * Use for persisting user preferences to localStorage or a backend.\n *\n * @example\n * ```typescript\n * const state = grid.getColumnState();\n * localStorage.setItem('gridState', JSON.stringify(state));\n * ```\n */\n getColumnState?(): GridColumnState;\n /**\n * Read the current column state. Property-style accessor that mirrors\n * {@link PublicGrid.getColumnState}. To restore state, use\n * {@link PublicGrid.applyColumnState}.\n *\n * @example\n * ```typescript\n * const snapshot = grid.columnState;\n * ```\n */\n columnState?: GridColumnState;\n\n /**\n * Apply a previously saved column state, restoring column order, widths,\n * visibility, sort, and any plugin-contributed state. Can be called before\n * or after grid initialization — pre-init calls are deferred and applied\n * during setup.\n *\n * @example\n * ```typescript\n * const saved = localStorage.getItem('gridState');\n * if (saved) grid.applyColumnState(JSON.parse(saved));\n * ```\n */\n applyColumnState?(state: GridColumnState | undefined): void;\n\n // Sort API\n /**\n * Get the current sort state.\n *\n * Returns `null` when no sort is active.\n *\n * @example\n * ```typescript\n * const sort = grid.sortModel;\n * // { field: 'id', direction: 'desc' } | null\n * ```\n */\n readonly sortModel?: { field: string; direction: 'asc' | 'desc' } | null;\n\n /**\n * Sort by a column, toggle a column's sort direction, or clear sorting.\n *\n * - `sort('id', 'desc')` — apply sort with explicit direction\n * - `sort('id')` — toggle: none → asc → desc → none\n * - `sort(null)` — clear sort, restore original row order\n *\n * @param field - Column field to sort by, or `null` to clear\n * @param direction - Explicit direction; omit to toggle\n *\n * @example\n * ```typescript\n * grid.sort('id', 'desc'); // sort descending\n * grid.sort('price'); // toggle sort on price\n * grid.sort(null); // clear sort\n * ```\n */\n sort?(field: string | null, direction?: 'asc' | 'desc'): void;\n\n // Loading API\n /**\n * Whether the grid is currently in a loading state.\n * When true, displays a loading overlay with spinner.\n *\n * Can also be set via the `loading` HTML attribute.\n *\n * @example\n * ```typescript\n * // Show loading overlay\n * grid.loading = true;\n * const data = await fetchData();\n * grid.rows = data;\n * grid.loading = false;\n * ```\n */\n loading?: boolean;\n\n /**\n * Set loading state for a specific row.\n * Displays a small spinner indicator on the row.\n *\n * Use when persisting row data or performing row-level async operations.\n *\n * @param rowId - The row's unique identifier (from getRowId)\n * @param loading - Whether the row is loading\n *\n * @example\n * ```typescript\n * // Show loading while saving row\n * grid.setRowLoading('emp-123', true);\n * await saveRow(row);\n * grid.setRowLoading('emp-123', false);\n * ```\n */\n setRowLoading?(rowId: string, loading: boolean): void;\n\n /**\n * Set loading state for a specific cell.\n * Displays a small spinner indicator on the cell.\n *\n * Use when performing cell-level async operations (e.g., validation, lookup).\n *\n * @param rowId - The row's unique identifier (from getRowId)\n * @param field - The column field\n * @param loading - Whether the cell is loading\n *\n * @example\n * ```typescript\n * // Show loading while validating cell\n * grid.setCellLoading('emp-123', 'email', true);\n * const isValid = await validateEmail(email);\n * grid.setCellLoading('emp-123', 'email', false);\n * ```\n */\n setCellLoading?(rowId: string, field: string, loading: boolean): void;\n\n /**\n * Check if a row is currently in loading state.\n * @param rowId - The row's unique identifier\n */\n isRowLoading?(rowId: string): boolean;\n\n /**\n * Check if a cell is currently in loading state.\n * @param rowId - The row's unique identifier\n * @param field - The column field\n */\n isCellLoading?(rowId: string, field: string): boolean;\n\n /**\n * Clear all row and cell loading states.\n */\n clearAllLoading?(): void;\n\n // Focus Management API\n /**\n * Register an external DOM element as a logical focus container of this grid.\n *\n * Focus moving into a registered container is treated as if it stayed inside\n * the grid: `data-has-focus` is preserved, click-outside commit is suppressed,\n * and the editing focus trap (when enabled) won't reclaim focus.\n *\n * Typical use case: overlay panels (datepickers, dropdowns, autocompletes)\n * that render at `<body>` level to escape grid overflow clipping.\n *\n * @param el - The external element to register\n *\n * @example\n * ```typescript\n * const overlay = document.createElement('div');\n * document.body.appendChild(overlay);\n *\n * // Tell the grid this overlay is \"part of\" the grid\n * grid.registerExternalFocusContainer(overlay);\n *\n * // Later, when overlay is removed\n * grid.unregisterExternalFocusContainer(overlay);\n * ```\n */\n registerExternalFocusContainer?(el: Element): void;\n\n /**\n * Unregister a previously registered external focus container.\n *\n * @param el - The element to unregister\n */\n unregisterExternalFocusContainer?(el: Element): void;\n\n /**\n * Check whether focus is logically inside this grid.\n *\n * Returns `true` when `document.activeElement` (or the given node) is\n * inside the grid's own DOM **or** inside any element registered via\n * {@link registerExternalFocusContainer}.\n *\n * @param node - Optional node to test. Defaults to `document.activeElement`.\n *\n * @example\n * ```typescript\n * if (grid.containsFocus()) {\n * console.log('Grid or one of its overlays has focus');\n * }\n * ```\n */\n containsFocus?(node?: Node | null): boolean;\n\n // Focus & Navigation API\n /**\n * Move focus to a specific cell.\n *\n * @param rowIndex - Row index (0-based, in the current processed row array)\n * @param column - Column index (0-based into visible columns) or field name\n */\n focusCell?(rowIndex: number, column: number | string): void;\n\n /**\n * The currently focused cell position, or `null` if no rows are loaded.\n */\n readonly focusedCell?: { rowIndex: number; colIndex: number; field: string } | null;\n\n /**\n * Scroll to make a row visible by its index.\n *\n * @param rowIndex - Row index (0-based, in the current processed row array)\n * @param options - Scroll alignment and behavior\n */\n scrollToRow?(rowIndex: number, options?: ScrollToRowOptions): void;\n\n /**\n * Scroll to make a row visible by its unique ID.\n *\n * @param rowId - The row's unique identifier (from {@link GridConfig.getRowId | getRowId})\n * @param options - Scroll alignment and behavior\n */\n scrollToRowById?(rowId: string, options?: ScrollToRowOptions): void;\n}\n// #endregion\n\n// #region InternalGrid Interface\n/**\n * Internal-only augmented interface for DataGrid component.\n *\n * Member prefixes indicate accessibility:\n * - `_underscore` = protected members - private outside core, accessible to plugins. Marked with @internal.\n * - `__doubleUnderscore` = deeply internal members - private outside core, only for internal functions.\n *\n * @category Plugin Development\n * @since 0.1.1\n */\nexport interface InternalGrid<T = any> extends PublicGrid<T>, GridConfig<T> {\n // Element methods available because DataGridElement extends HTMLElement\n /** The element's `id` attribute. Available because DataGridElement extends HTMLElement. */\n id: string;\n /**\n * The grid's host HTMLElement (`this`). Use instead of casting `grid as unknown as HTMLElement`.\n * @internal\n */\n readonly _hostElement: HTMLElement;\n querySelector<K extends keyof HTMLElementTagNameMap>(selectors: K): HTMLElementTagNameMap[K] | null;\n querySelector<E extends Element = Element>(selectors: string): E | null;\n querySelectorAll<K extends keyof HTMLElementTagNameMap>(selectors: K): NodeListOf<HTMLElementTagNameMap[K]>;\n querySelectorAll<E extends Element = Element>(selectors: string): NodeListOf<E>;\n _rows: T[];\n _columns: ColumnInternal<T>[];\n /** Visible columns only (excludes hidden). Use for rendering. */\n _visibleColumns: ColumnInternal<T>[];\n _headerRowEl: HTMLElement;\n _bodyEl: HTMLElement;\n _rowPool: RowElementInternal[];\n _resizeController: ResizeController;\n _sortState: { field: string; direction: 1 | -1 } | null;\n /** Original unfiltered/unprocessed rows. @internal */\n sourceRows: T[];\n /** Framework adapter instance (set by Grid directives). @internal */\n __frameworkAdapter?: FrameworkAdapter;\n __originalOrder: T[];\n __rowRenderEpoch: number;\n __didInitialAutoSize?: boolean;\n __lightDomColumnsCache?: ColumnInternal[];\n __originalColumnNodes?: HTMLElement[];\n /** Cell display value cache. @internal */\n __cellDisplayCache?: Map<number, string[]>;\n /** Cache epoch for cell display values. @internal */\n __cellCacheEpoch?: number;\n /** Cached header row count for virtualization. @internal */\n __cachedHeaderRowCount?: number;\n /** Cached flag for whether grid has special columns (custom renderers, etc.). @internal */\n __hasSpecialColumns?: boolean;\n /** Cached flag for whether any plugin has renderRow hooks. @internal */\n __hasRenderRowPlugins?: boolean;\n /** @internal Access the plugin manager's cached state. */\n _pluginManager?: {\n _hasRowStructurePlugins: boolean;\n /** Emit an event on the plugin event bus (does not dispatch a DOM event). */\n emitPluginEvent?: <D>(eventType: string, detail: D) => void;\n /** Let plugins contribute to the effective config during merge (mutates in place). */\n processConfig?(config: GridConfig): void;\n };\n _gridTemplate: string;\n _virtualization: VirtualState;\n _focusRow: number;\n _focusCol: number;\n /** Currently active edit row index. Injected by EditingPlugin. @internal */\n _activeEditRows?: number;\n /** Whether the grid is in 'grid' editing mode (all rows editable). Injected by EditingPlugin. @internal */\n _isGridEditMode?: boolean;\n /** Snapshots of row data before editing. Injected by EditingPlugin. @internal */\n _rowEditSnapshots?: Map<number, T>;\n /** Get all changed rows. Injected by EditingPlugin. */\n changedRows?: T[];\n /** Get IDs of all changed rows. Injected by EditingPlugin. */\n changedRowIds?: string[];\n /** Internal Set for O(1) lookup in the render hot path. Injected by EditingPlugin. @internal */\n _changedRowIdSet?: ReadonlySet<string>;\n effectiveConfig?: GridConfig<T>;\n findHeaderRow?: () => HTMLElement;\n refreshVirtualWindow: (full: boolean, skipAfterRender?: boolean) => boolean;\n /** @internal Trigger a COLUMNS-phase re-render. */\n refreshColumns?: () => void;\n updateTemplate?: () => void;\n findRenderedRowElement?: (rowIndex: number) => HTMLElement | null;\n /** Get a row by its ID. Implemented in grid.ts */\n getRow?: (id: string) => T | undefined;\n /** Get a row and its current index by ID. Returns undefined if not found. @internal */\n _getRowEntry: (id: string) => { row: T; index: number } | undefined;\n /** Get the unique ID for a row. Implemented in grid.ts */\n getRowId?: (row: T) => string;\n /** Update a row by ID. Implemented in grid.ts */\n updateRow?: (id: string, changes: Partial<T>, source?: UpdateSource) => void;\n /** Animate a single row. Returns Promise that resolves when animation completes. Implemented in grid.ts */\n animateRow?: (rowIndex: number, type: RowAnimationType) => Promise<boolean>;\n /** Animate multiple rows. Returns Promise that resolves when all animations complete. Implemented in grid.ts */\n animateRows?: (rowIndices: number[], type: RowAnimationType) => Promise<number>;\n /** Animate a row by its ID. Returns Promise that resolves when animation completes. Implemented in grid.ts */\n animateRowById?: (rowId: string, type: RowAnimationType) => Promise<boolean>;\n /** Begin bulk edit on a row. Injected by EditingPlugin. */\n beginBulkEdit?: (rowIndex: number) => void;\n /** Commit active row edit. Injected by EditingPlugin. */\n commitActiveRowEdit?: () => void;\n /** Dispatch cell click to plugin system, returns true if handled */\n _dispatchCellClick?: (event: MouseEvent, rowIndex: number, colIndex: number, cellEl: HTMLElement) => boolean;\n /** Dispatch row click to plugin system, returns true if handled */\n _dispatchRowClick?: (event: MouseEvent, rowIndex: number, row: any, rowEl: HTMLElement) => boolean;\n /** Dispatch header click to plugin system, returns true if handled */\n _dispatchHeaderClick?: (event: MouseEvent | KeyboardEvent, col: ColumnConfig, headerEl: HTMLElement) => boolean;\n /** Dispatch keydown to plugin system, returns true if handled */\n _dispatchKeyDown?: (event: KeyboardEvent) => boolean;\n /** Dispatch cell mouse events for drag operations. Returns true if any plugin started a drag. */\n _dispatchCellMouseDown?: (event: CellMouseEvent) => boolean;\n /** Dispatch cell mouse move during drag. */\n _dispatchCellMouseMove?: (event: CellMouseEvent) => void;\n /** Dispatch cell mouse up to end drag. */\n _dispatchCellMouseUp?: (event: CellMouseEvent) => void;\n /** Call afterCellRender hook on all plugins. Called from rows.ts after each cell is rendered. @internal */\n _afterCellRender?: (context: AfterCellRenderContext<T>) => void;\n /** Check if any plugin has registered an afterCellRender hook. Used to skip hook call for performance. @internal */\n _hasAfterCellRenderHook?: () => boolean;\n /** Call afterRowRender hook on all plugins. Called from rows.ts after each row is rendered. @internal */\n _afterRowRender?: (context: AfterRowRenderContext<T>) => void;\n /** Check if any plugin has registered an afterRowRender hook. Used to skip hook call for performance. @internal */\n _hasAfterRowRenderHook?: () => boolean;\n /** Get horizontal scroll boundary offsets from plugins */\n _getHorizontalScrollOffsets?: (\n rowEl?: HTMLElement,\n focusedCell?: HTMLElement,\n ) => { left: number; right: number; skipScroll?: boolean };\n /** Request emission of column-state-change event (debounced) */\n requestStateChange?: () => void;\n\n // Methods exposed for extracted managers (VirtualizationManager, FocusManager, RowManager, RenderScheduler)\n /** @internal Clear the cached _visibleColumns array so the next read recomputes from _columns. */\n _invalidateVisibleColumnsCache(): void;\n /** @internal */ _renderVisibleRows(start: number, end: number, epoch?: number): void;\n /** @internal */ _updateAriaCounts(totalRows: number, totalCols: number): void;\n /** @internal */ _requestSchedulerPhase(phase: RenderPhase, source: string): void;\n /** @internal */ _rebuildRowIdMap(): void;\n /** @internal */ _emitDataChange(): void;\n /** @internal */ _getPluginRowHeight(row: T, index: number): number | undefined;\n /** @internal */ _adjustPluginVirtualStart(start: number, scrollTop: number, rowHeight: number): number | undefined;\n /** @internal */ _afterPluginRender(): void;\n /** @internal */ _emitPluginEvent(event: string, detail: unknown): void;\n\n // Scheduler pipeline callbacks\n /** @internal */ _schedulerMergeConfig(): void;\n /** @internal */ _schedulerProcessColumns(): void;\n /** @internal */ _schedulerProcessRows(): void;\n /** @internal */ _schedulerRenderHeader(): void;\n /** @internal */ _schedulerUpdateTemplate(): void;\n /** @internal */ _schedulerAfterRender(): void;\n /** @internal */ readonly _schedulerIsConnected: boolean;\n\n // Shell controller & config manager support\n /** @internal The render root element for DOM queries. */\n readonly _renderRoot: Element;\n /** @internal Emit a custom event from the grid. */\n _emit(eventName: string, detail: unknown): void;\n /** @internal Get accordion expand/collapse icons from effective config. */\n readonly _accordionIcons: { expand: IconValue; collapse: IconValue };\n /** @internal Clear the row pool and body element. */\n _clearRowPool(): void;\n /** @internal Run grid setup (DOM rebuild). */\n _setup(): void;\n /** @internal Apply animation configuration to host element. */\n _applyAnimationConfig(config: GridConfig): void;\n}\n\n/**\n * Grid reference type combining InternalGrid with HTMLElement.\n * Used by extracted managers that need both internal grid state and DOM APIs.\n * @internal\n */\nexport type GridHost<T = any> = InternalGrid<T> & HTMLElement;\n// #endregion\n\n// #region Column Types\n/**\n * Built-in primitive column types with automatic formatting and editing support.\n *\n * - `'string'` - Text content, default text input editor\n * - `'number'` - Numeric content, right-aligned, number input editor\n * - `'date'` - Date content, formatted display, date picker editor\n * - `'boolean'` - True/false, rendered as checkbox\n * - `'select'` - Dropdown selection from `options` array\n *\n * @example\n * ```typescript\n * columns: [\n * { field: 'name', type: 'string' },\n * { field: 'age', type: 'number' },\n * { field: 'hireDate', type: 'date' },\n * { field: 'active', type: 'boolean' },\n * { field: 'department', type: 'select', options: [\n * { label: 'Engineering', value: 'eng' },\n * { label: 'Sales', value: 'sales' },\n * ]},\n * ]\n * ```\n *\n * @see {@link ColumnType} for custom type support\n * @see {@link TypeDefault} for type-level defaults\n * @since 0.1.1\n */\nexport type PrimitiveColumnType = 'number' | 'string' | 'date' | 'boolean' | 'select';\n\n/**\n * Column type - built-in primitives or custom type strings.\n *\n * Use built-in types for automatic formatting, or define custom types\n * (e.g., 'currency', 'country') with type-level defaults via `typeDefaults`.\n *\n * @example\n * ```typescript\n * // Built-in types\n * { field: 'name', type: 'string' }\n * { field: 'salary', type: 'number' }\n *\n * // Custom types with defaults\n * grid.gridConfig = {\n * columns: [\n * { field: 'salary', type: 'currency' },\n * { field: 'birthCountry', type: 'country' },\n * ],\n * typeDefaults: {\n * currency: {\n * format: (v) => `$${Number(v).toFixed(2)}`,\n * },\n * country: {\n * renderer: (ctx) => `🌍 ${ctx.value}`,\n * },\n * },\n * };\n * ```\n *\n * @see {@link PrimitiveColumnType} for built-in types\n * @see {@link TypeDefault} for defining custom type defaults\n * @since 1.0.0\n */\nexport type ColumnType = PrimitiveColumnType | (string & {});\n// #endregion\n\n// #region TypeDefault Interface\n/**\n * Type-level defaults for formatters and renderers.\n * Applied to all columns of a given type unless overridden at column level.\n *\n * Note: `editor` and `editorParams` are added via module augmentation when\n * EditingPlugin is imported. `filterPanelRenderer` is added by FilteringPlugin.\n *\n * @example\n * ```typescript\n * typeDefaults: {\n * currency: {\n * format: (value) => new Intl.NumberFormat('en-US', {\n * style: 'currency',\n * currency: 'USD',\n * }).format(value as number),\n * },\n * country: {\n * renderer: (ctx) => {\n * const span = document.createElement('span');\n * span.innerHTML = `<img src=\"/flags/${ctx.value}.svg\" /> ${ctx.value}`;\n * return span;\n * }\n * }\n * }\n * ```\n *\n * @see {@link ColumnViewRenderer} for custom renderer function signature\n * @see {@link ColumnType} for type strings that can have defaults\n * @see {@link GridConfig.typeDefaults} for registering type defaults\n * @since 1.0.0\n */\nexport interface TypeDefault<TRow = unknown> {\n /**\n * Default formatter for all columns of this type.\n *\n * Transforms the raw cell value into a display string. Use when you need\n * consistent formatting across columns without custom DOM (e.g., currency,\n * percentages, dates with specific locale).\n *\n * **Resolution Priority**: Column `format` → Type `format` → Built-in\n *\n * @example\n * ```typescript\n * typeDefaults: {\n * currency: {\n * format: (value) => new Intl.NumberFormat('en-US', {\n * style: 'currency',\n * currency: 'USD',\n * }).format(value as number),\n * },\n * percentage: {\n * format: (value) => `${(value as number * 100).toFixed(1)}%`,\n * }\n * }\n * ```\n */\n format?: (value: unknown, row: TRow) => string;\n\n /**\n * Default renderer for all columns of this type.\n *\n * Creates custom DOM for the cell content. Use when you need more than\n * text formatting (e.g., icons, badges, interactive elements).\n *\n * **Resolution Priority**: Column `renderer` → Type `renderer` → App-level (adapter) → Built-in\n *\n * @example\n * ```typescript\n * typeDefaults: {\n * status: {\n * renderer: (ctx) => {\n * const badge = document.createElement('span');\n * badge.className = `badge badge-${ctx.value}`;\n * badge.textContent = ctx.value as string;\n * return badge;\n * }\n * },\n * country: {\n * renderer: (ctx) => {\n * const span = document.createElement('span');\n * span.innerHTML = `<img src=\"/flags/${ctx.value}.svg\" /> ${ctx.value}`;\n * return span;\n * }\n * }\n * }\n * ```\n */\n renderer?: ColumnViewRenderer<TRow, unknown>;\n}\n// #endregion\n\n// #region BaseColumnConfig Interface\n/**\n * Base contract for a column configuration.\n *\n * Defines the fundamental properties all columns share. Extended by {@link ColumnConfig}\n * with additional features like custom renderers and grouping.\n *\n * @example\n * ```typescript\n * // Basic column with common properties\n * const columns: BaseColumnConfig<Employee>[] = [\n * {\n * field: 'name',\n * header: 'Full Name',\n * sortable: true,\n * resizable: true,\n * },\n * {\n * field: 'salary',\n * type: 'number',\n * width: 120,\n * format: (value) => `$${value.toLocaleString()}`,\n * sortComparator: (a, b) => a - b,\n * },\n * {\n * field: 'department',\n * type: 'select',\n * options: [\n * { label: 'Engineering', value: 'eng' },\n * { label: 'Sales', value: 'sales' },\n * ],\n * },\n * ];\n * ```\n *\n * @see {@link ColumnConfig} for full column configuration with renderers\n * @see {@link ColumnType} for type options\n * @since 0.1.1\n */\nexport interface BaseColumnConfig<TRow = any, TValue = any> {\n /** Unique field key referencing property in row objects */\n field: keyof TRow & string;\n /** Visible header label; defaults to capitalized field */\n header?: string;\n /**\n * Column data type.\n *\n * Built-in types: `'string'`, `'number'`, `'date'`, `'boolean'`, `'select'`\n *\n * Custom types (e.g., `'currency'`, `'country'`) can have type-level defaults\n * via `gridConfig.typeDefaults` or framework adapter registries.\n *\n * @default Inferred from first row data\n */\n type?: ColumnType;\n /** Column width in pixels; fixed size (no flexibility) */\n width?: string | number;\n /** Minimum column width in pixels; in stretch mode uses minmax(minWidth, 1fr), and in fixed mode is used as implicit width when width is omitted */\n minWidth?: number;\n /**\n * Initial column display index.\n *\n * Sets the column's initial position in the grid declaratively. When `order` is set,\n * the column is repositioned at that index among the initial column array, _before_\n * any saved column state is applied.\n *\n * **Precedence** (low → high):\n * ```\n * data-key order (merge) < order attribute < columnState / applyColumnState / runtime reorder\n * / array order (auto)\n * ```\n *\n * If the user supplies saved column state (`gridConfig.columnState`, `applyColumnState()`,\n * or runtime reorder via drag), that state **wins**. Use `resetColumnOrder()` to return\n * to the `order`-influenced initial order.\n *\n * @example\n * ```typescript\n * // Move 'special-field' to initial index 1; other columns stay inferred\n * <tbw-grid data-src=\"/url\" column-inference=\"merge\">\n * <tbw-grid-column field=\"special-field\" order=\"1\"></tbw-grid-column>\n * </tbw-grid>\n * ```\n *\n * @see ReorderPlugin.resetColumnOrder() — returns to this order\n * @since 2.17.0\n */\n order?: number;\n /** Whether column can be sorted */\n sortable?: boolean;\n /** Whether column can be resized by user */\n resizable?: boolean;\n /** Optional custom comparator for sorting (a,b) -> number */\n sortComparator?: (a: TValue, b: TValue, rowA: TRow, rowB: TRow) => number;\n /**\n * Compute the cell's value from the row. When defined, this is the single\n * source of truth used by sorting, filtering, formatting, cell rendering,\n * export, and clipboard — eliminating the need to duplicate value-extraction\n * logic across `sortComparator`, `filterValue`, and per-renderer code.\n *\n * **Resolution precedence**:\n * - **Sort**: `sortComparator` → `valueAccessor` → `row[field]`\n * - **Filter**: `filterValue` → `valueAccessor` → `row[field]`\n * - **Render / format / export / copy**: `valueAccessor` → `row[field]`\n *\n * The accessor is the *default* value source — per-column escape hatches\n * (`sortComparator`, `filterValue`) still take precedence when set.\n *\n * Results are memoized per `(row identity, column field)` so accessors are\n * free to be \"slow but correct\" (e.g. `array.find(...)`). Immutable row\n * updates auto-invalidate; in-place mutations are invalidated by the grid's\n * edit / transaction paths.\n *\n * Note: a `valueAccessor` without a paired `valueSetter` (planned API)\n * implies the column is read-only — editors will not commit through it.\n *\n * @example\n * ```typescript\n * // Computed value from nested data\n * {\n * field: 'bolDate',\n * header: 'BL date',\n * valueAccessor: ({ row }) => {\n * if (isCargo(row)) {\n * return row.movements.find(m => m.operationType === 'LOAD')?.movementDate ?? null;\n * }\n * return row.movementDate ?? null;\n * },\n * filterType: 'date',\n * // No need for sortComparator or filterValue — they fall back to the accessor.\n * }\n * ```\n */\n valueAccessor?: (ctx: { row: TRow; column: ColumnConfig<TRow>; rowIndex: number }) => TValue;\n /** For select type - available options */\n options?: Array<{ label: string; value: unknown }> | (() => Array<{ label: string; value: unknown }>);\n /**\n * Formats the raw cell value into a display string.\n *\n * Used both for **cell rendering** and the **built-in filter panel**:\n * - In cells, the formatted value replaces the raw value as text content.\n * - In the filter panel (set filter), checkbox labels show the formatted value\n * instead of the raw value, and search matches against the formatted text.\n *\n * The `row` parameter is available during cell rendering but is `undefined`\n * when called from the filter panel (standalone value formatting). Avoid\n * accessing `row` properties in format functions intended for filter display.\n *\n * @example\n * ```typescript\n * // Currency formatter — works in both cells and filter panel\n * {\n * field: 'price',\n * format: (value) => `$${Number(value).toFixed(2)}`,\n * }\n *\n * // ID-to-name lookup — filter panel shows names, not IDs\n * {\n * field: 'departmentId',\n * format: (value) => departmentMap.get(value as string) ?? String(value),\n * }\n * ```\n */\n format?: (value: TValue, row: TRow) => string;\n /**\n * Marks this column as a **system / utility column** — a column that exists to\n * support grid behaviour rather than to display user data.\n *\n * Built-in plugins set this on the columns they synthesize (selection checkbox,\n * row-reorder drag handle, master-detail / tree / row-grouping expander). You can\n * also set it on columns you author yourself when you want them to behave the same\n * way — for example, a row-action menu column, a status indicator, or any\n * developer-defined \"system\" column that should be visible in the grid only.\n *\n * Setting `utility: true` excludes the column from:\n *\n * - **Visibility panel** — not listed in the show/hide UI\n * - **Column reorder** — header drag-drop and visibility-panel drag treat it as locked\n * (equivalent to `lockPosition: true`)\n * - **Print** — hidden by `PrintPlugin` during print\n * - **Clipboard copy** — skipped by `ClipboardPlugin`\n * - **Export** (CSV / JSON / Excel) — skipped by `ExportPlugin`\n * - **Range / row selection** — clicks land on the column but selection ignores it\n * - **Filter UI** — no filter button rendered, no filter model entry\n *\n * The column is still rendered in the grid and still receives `cellRenderer` /\n * `viewRenderer` / `headerRenderer` callbacks — it is \"hidden from the system,\n * visible in the grid\".\n *\n * Convention: name the field with a `__`-prefix (e.g. `__actions`) so it cannot\n * collide with a real data field.\n *\n * @example A custom row-actions column\n * ```ts\n * {\n * field: '__actions',\n * header: '',\n * width: 80,\n * utility: true, // excluded from print, export, reorder, visibility, etc.\n * resizable: false,\n * sortable: false,\n * filterable: false,\n * viewRenderer: ({ row }) => createActionsButton(row),\n * }\n * ```\n */\n utility?: boolean;\n /**\n * Arbitrary extra metadata for application use.\n *\n * @remarks\n * **Do not use `meta` for grid-recognized flags.** Properties like `lockPosition`,\n * `lockVisible`, `lockPinning`, `pinned`, `utility`, and `checkboxColumn` are first-class\n * augmented properties on `ColumnConfig` itself. Using `meta.<flag>` for any of them is\n * deprecated and only kept as a runtime fallback for back-compat.\n */\n meta?: Record<string, unknown>;\n}\n// #endregion\n\n// #region ColumnConfig Interface\n/**\n * Full column configuration including custom renderers, editors, and grouping metadata.\n *\n * Extends {@link BaseColumnConfig} with additional features for customizing\n * how cells are displayed and edited.\n *\n * @example\n * ```typescript\n * const columns: ColumnConfig<Employee>[] = [\n * // Basic sortable column\n * { field: 'id', header: 'ID', width: 60, sortable: true },\n *\n * // Column with custom renderer\n * {\n * field: 'name',\n * header: 'Employee',\n * renderer: (ctx) => {\n * const div = document.createElement('div');\n * div.innerHTML = `<img src=\"${ctx.row.avatar}\" /><span>${ctx.value}</span>`;\n * return div;\n * },\n * },\n *\n * // Column with custom header\n * {\n * field: 'email',\n * headerLabelRenderer: (ctx) => `${ctx.value} 📧`,\n * },\n *\n * // Editable column (requires EditingPlugin)\n * {\n * field: 'status',\n * editable: true,\n * editor: (ctx) => {\n * const select = document.createElement('select');\n * // ... editor implementation\n * return select;\n * },\n * },\n *\n * // Hidden column (can be shown via VisibilityPlugin)\n * { field: 'internalNotes', hidden: true },\n * ];\n * ```\n *\n * @see {@link BaseColumnConfig} for basic column properties\n * @see {@link ColumnViewRenderer} for custom cell renderers\n * @see {@link ColumnEditorSpec} for custom cell editors\n * @see {@link HeaderRenderer} for custom header renderers\n * @since 0.1.1\n */\nexport interface ColumnConfig<TRow = any> extends BaseColumnConfig<TRow, any> {\n /**\n * Optional custom cell renderer function. Alias for `viewRenderer`.\n * Can return an HTMLElement, a Node, or an HTML string (which will be sanitized).\n *\n * @example\n * ```typescript\n * // Simple string template\n * renderer: (ctx) => `<span class=\"badge\">${ctx.value}</span>`\n *\n * // DOM element\n * renderer: (ctx) => {\n * const el = document.createElement('span');\n * el.textContent = ctx.value;\n * return el;\n * }\n * ```\n */\n renderer?: ColumnViewRenderer<TRow, any>;\n /** Optional custom view renderer used instead of default text rendering */\n viewRenderer?: ColumnViewRenderer<TRow, any>;\n /** External view spec (lets host app mount any framework component) */\n externalView?: {\n component: unknown;\n props?: Record<string, unknown>;\n mount?: (options: {\n placeholder: HTMLElement;\n context: CellRenderContext<TRow, unknown>;\n spec: unknown;\n }) => void | { dispose?: () => void };\n };\n /** Whether the column is initially hidden */\n hidden?: boolean;\n /** Prevent this column from being hidden programmatically */\n lockVisible?: boolean;\n /**\n * Dynamic CSS class(es) for cells in this column.\n * Called for each cell during rendering. Return class names to add to the cell element.\n *\n * @example\n * ```typescript\n * // Highlight negative values\n * cellClass: (value, row, column) => value < 0 ? ['negative', 'text-red'] : []\n *\n * // Status-based styling\n * cellClass: (value) => [`status-${value}`]\n *\n * // Single class as string\n * cellClass: (value) => value < 0 ? 'negative' : ''\n * ```\n */\n cellClass?: (value: unknown, row: TRow, column: ColumnConfig<TRow>) => string | string[];\n\n /**\n * Custom header label renderer. Customize the label content while the grid\n * handles sort icons, filter buttons, resize handles, and click interactions.\n *\n * Use this for simple customizations like adding icons, badges, or units.\n *\n * @example\n * ```typescript\n * // Add required field indicator\n * headerLabelRenderer: (ctx) => `${ctx.value} <span class=\"required\">*</span>`\n *\n * // Add unit to header\n * headerLabelRenderer: (ctx) => {\n * const span = document.createElement('span');\n * span.innerHTML = `${ctx.value}<br/><small>(kg)</small>`;\n * return span;\n * }\n * ```\n */\n headerLabelRenderer?: HeaderLabelRenderer<TRow>;\n\n /**\n * Custom header cell renderer. Complete control over the entire header cell.\n * Resize handles are added automatically for resizable columns.\n *\n * The context provides helper functions to include standard elements:\n * - `renderSortIcon()` - Returns sort indicator element (null if not sortable)\n * - `renderFilterButton()` - Returns filter button (null if not filterable)\n *\n * **Precedence**: `headerRenderer` > `headerLabelRenderer` > `header` > `field`\n *\n * @example\n * ```typescript\n * headerRenderer: (ctx) => {\n * const div = document.createElement('div');\n * div.className = 'custom-header';\n * div.innerHTML = `<span>${ctx.value}</span>`;\n * const sortIcon = ctx.renderSortIcon();\n * if (sortIcon) div.appendChild(sortIcon);\n * return div;\n * }\n * ```\n */\n headerRenderer?: HeaderRenderer<TRow>;\n}\n// #endregion\n\n// #region ColumnConfigMap Type\n/**\n * Array of column configurations.\n * Convenience type alias for `ColumnConfig<TRow>[]`.\n *\n * @example\n * ```typescript\n * const columns: ColumnConfigMap<Employee> = [\n * { field: 'name', header: 'Full Name', sortable: true },\n * { field: 'email', header: 'Email Address' },\n * { field: 'department', type: 'select', options: deptOptions },\n * ];\n *\n * grid.columns = columns;\n * ```\n *\n * @see {@link ColumnConfig} for individual column options\n * @see {@link GridConfig.columns} for setting columns on the grid\n * @since 0.1.1\n */\nexport type ColumnConfigMap<TRow = any> = ColumnConfig<TRow>[];\n// #endregion\n\n// #region Editor Types\n/**\n * Editor specification for inline cell editing.\n * Supports multiple formats for maximum flexibility.\n *\n * **Format Options:**\n * - `string` - Custom element tag name (e.g., 'my-date-picker')\n * - `function` - Factory function returning an editor element\n * - `object` - External component spec for framework integration\n *\n * @example\n * ```typescript\n * // 1. Custom element tag name\n * columns: [\n * { field: 'date', editor: 'my-date-picker' }\n * ]\n *\n * // 2. Factory function (full control)\n * columns: [\n * {\n * field: 'status',\n * editor: (ctx) => {\n * const select = document.createElement('select');\n * select.innerHTML = `\n * <option value=\"active\">Active</option>\n * <option value=\"inactive\">Inactive</option>\n * `;\n * select.value = ctx.value;\n * select.onchange = () => ctx.commit(select.value);\n * select.onkeydown = (e) => {\n * if (e.key === 'Escape') ctx.cancel();\n * };\n * return select;\n * }\n * }\n * ]\n *\n * // 3. External component (React, Angular, Vue)\n * columns: [\n * {\n * field: 'country',\n * editor: {\n * component: CountrySelect,\n * props: { showFlags: true }\n * }\n * }\n * ]\n * ```\n *\n * @see {@link ColumnEditorContext} for the context passed to factory functions\n * @since 0.1.1\n */\nexport type ColumnEditorSpec<TRow = unknown, TValue = unknown> =\n | string // custom element tag name\n | ((context: ColumnEditorContext<TRow, TValue>) => HTMLElement | string)\n | {\n /** Arbitrary component reference (class, function, token) */\n component: unknown;\n /** Optional static props passed to mount */\n props?: Record<string, unknown>;\n /** Optional custom mount function; if provided we call it directly instead of emitting an event */\n mount?: (options: {\n placeholder: HTMLElement;\n context: ColumnEditorContext<TRow, TValue>;\n spec: unknown;\n }) => void | { dispose?: () => void };\n };\n\n/**\n * Context object provided to editor factories allowing mutation (commit/cancel) of a cell value.\n *\n * The `commit` and `cancel` functions control the editing lifecycle:\n * - Call `commit(newValue)` to save changes and exit edit mode\n * - Call `cancel()` to discard changes and exit edit mode\n *\n * @example\n * ```typescript\n * const myEditor: ColumnEditorSpec = (ctx: ColumnEditorContext) => {\n * const input = document.createElement('input');\n * input.value = ctx.value;\n * input.className = 'my-editor';\n *\n * // Save on Enter, cancel on Escape\n * input.onkeydown = (e) => {\n * if (e.key === 'Enter') {\n * ctx.commit(input.value);\n * } else if (e.key === 'Escape') {\n * ctx.cancel();\n * }\n * };\n *\n * // Access row data for validation\n * if (ctx.row.locked) {\n * input.disabled = true;\n * }\n *\n * return input;\n * };\n * ```\n *\n * @see {@link ColumnEditorSpec} for editor specification options\n * @since 0.1.1\n */\nexport interface ColumnEditorContext<TRow = any, TValue = any> {\n /** Underlying full row object for the active edit. */\n row: TRow;\n /** Current cell value (mutable only via commit). */\n value: TValue;\n /** Field name being edited. */\n field: keyof TRow & string;\n /** Column configuration reference. */\n column: ColumnConfig<TRow>;\n /**\n * Stable row identifier (from `getRowId`).\n * Empty string if no `getRowId` is configured.\n */\n rowId: string;\n /** Accept the edit; triggers change tracking + rerender. */\n commit: (newValue: TValue) => void;\n /** Abort edit without persisting changes. */\n cancel: () => void;\n /**\n * Update other fields in this row while the editor is open.\n * Changes are committed with source `'cascade'`, triggering\n * `cell-change` events and `onValueChange` pushes to sibling editors.\n *\n * Useful for editors that affect multiple fields (e.g., an address\n * lookup that sets city + zip + state).\n *\n * @example\n * ```typescript\n * // In a cell-commit listener:\n * grid.on('cell-commit', (detail) => {\n * if (detail.field === 'quantity') {\n * detail.updateRow({ total: detail.row.price * detail.value });\n * }\n * });\n * ```\n */\n updateRow: (changes: Partial<TRow>) => void;\n /**\n * Register a callback invoked when the cell's underlying value changes\n * while the editor is open (e.g., via `updateRow()` from another cell's commit).\n *\n * Built-in editors auto-update their input values. Custom/framework editors\n * should use this to stay in sync with external mutations.\n *\n * @example\n * ```typescript\n * const editor = (ctx: ColumnEditorContext) => {\n * const input = document.createElement('input');\n * input.value = String(ctx.value);\n * ctx.onValueChange?.((newValue) => {\n * input.value = String(newValue);\n * });\n * return input;\n * };\n * ```\n */\n onValueChange?: (callback: (newValue: TValue) => void) => void;\n /**\n * The grid element that owns this editor.\n *\n * Use to access public grid API from custom editors — e.g.\n * `grid.registerExternalFocusContainer(panel)` so the grid treats focus\n * inside a portal-rendered overlay (Autocomplete, date picker, color\n * picker) as \"still inside the editor\" and does not exit row edit on\n * click. Mirrors {@link CellRenderContext.grid} for renderers.\n *\n * Always populated when the editor is mounted via the grid's editing\n * pipeline. Optional in the type for backwards compatibility with\n * factory functions written against the original signature.\n *\n * @example\n * ```typescript\n * const editor: ColumnEditorSpec = (ctx: ColumnEditorContext) => {\n * const panel = document.createElement('div');\n * document.body.appendChild(panel);\n * ctx.grid?.registerExternalFocusContainer(panel);\n * // ...\n * };\n * ```\n *\n * @see {@link CellRenderContext.grid}\n */\n grid?: PublicGrid<TRow> & HTMLElement;\n}\n// #endregion\n\n// #region Renderer Types\n/**\n * Context passed to custom view renderers (pure display – no commit helpers).\n *\n * Used by `viewRenderer` and `renderer` column properties to create\n * custom cell content. Return a DOM node or HTML string.\n *\n * @example\n * ```typescript\n * // Status badge renderer\n * const statusRenderer: ColumnViewRenderer = (ctx: CellRenderContext) => {\n * const badge = document.createElement('span');\n * badge.className = `badge badge-${ctx.value}`;\n * badge.textContent = ctx.value;\n * return badge;\n * };\n *\n * // Progress bar using row data\n * const progressRenderer: ColumnViewRenderer = (ctx) => {\n * const bar = document.createElement('div');\n * bar.className = 'progress-bar';\n * bar.style.width = `${ctx.value}%`;\n * bar.title = `${ctx.row.taskName}: ${ctx.value}%`;\n * return bar;\n * };\n *\n * // Return HTML string (simpler, less performant)\n * const htmlRenderer: ColumnViewRenderer = (ctx) => {\n * return `<strong>${ctx.value}</strong>`;\n * };\n * ```\n *\n * @see {@link ColumnViewRenderer} for the renderer function signature\n * @since 0.1.1\n */\nexport interface CellRenderContext<TRow = any, TValue = any> {\n /** Row object for the cell being rendered. */\n row: TRow;\n /** Value at field. */\n value: TValue;\n /** Field key. */\n field: keyof TRow & string;\n /** Column configuration reference. */\n column: ColumnConfig<TRow>;\n /**\n * The grid element that owns this cell.\n * Use to access public grid API (e.g., `getPluginByName()`) from custom renderers.\n *\n * @example\n * ```typescript\n * const renderer: ColumnViewRenderer<MyRow> = (ctx) => {\n * const tree = ctx.grid?.getPluginByName('tree');\n * // ...\n * };\n * ```\n */\n grid?: PublicGrid<TRow> & HTMLElement;\n /**\n * The cell DOM element being rendered into.\n * Framework adapters can use this to cache per-cell state (e.g., React roots).\n * @internal\n */\n cellEl?: HTMLElement;\n}\n\n/**\n * Custom view renderer function for cell content.\n *\n * Returns one of:\n * - `Node` - DOM element to display in the cell\n * - `string` - HTML string (parsed and inserted)\n * - `void | null` - Use default text rendering\n *\n * @example\n * ```typescript\n * // DOM element (recommended for interactivity)\n * const avatarRenderer: ColumnViewRenderer<Employee> = (ctx) => {\n * const img = document.createElement('img');\n * img.src = ctx.row.avatarUrl;\n * img.alt = ctx.row.name;\n * img.className = 'avatar';\n * return img;\n * };\n *\n * // HTML string (simpler, good for static content)\n * const emailRenderer: ColumnViewRenderer = (ctx) => {\n * return `<a href=\"mailto:${ctx.value}\">${ctx.value}</a>`;\n * };\n *\n * // Conditional rendering\n * const conditionalRenderer: ColumnViewRenderer = (ctx) => {\n * if (!ctx.value) return null; // Use default\n * return `<em>${ctx.value}</em>`;\n * };\n * ```\n *\n * @see {@link CellRenderContext} for available context properties\n * @since 0.1.1\n */\nexport type ColumnViewRenderer<TRow = unknown, TValue = unknown> = (\n ctx: CellRenderContext<TRow, TValue>,\n) => Node | string | void | null;\n// #endregion\n\n// #region Header Renderer Types\n/**\n * Context passed to `headerLabelRenderer` for customizing header label content.\n * The framework handles sort icons, filter buttons, resize handles, and click interactions.\n *\n * @example\n * ```typescript\n * headerLabelRenderer: (ctx) => {\n * const span = document.createElement('span');\n * span.innerHTML = `${ctx.value} <span class=\"required\">*</span>`;\n * return span;\n * }\n * ```\n * @since 1.4.0\n */\nexport interface HeaderLabelContext<TRow = unknown> {\n /** Column configuration reference. */\n column: ColumnConfig<TRow>;\n /** The header text (from column.header or column.field). */\n value: string;\n}\n\n/**\n * Context passed to `headerRenderer` for complete control over header cell content.\n * When using this, you control the header content. Resize handles are added automatically\n * for resizable columns.\n *\n * @example\n * ```typescript\n * headerRenderer: (ctx) => {\n * const div = document.createElement('div');\n * div.className = 'custom-header';\n * div.innerHTML = `<span>${ctx.value}</span>`;\n * // Optionally include sort icon\n * const sortIcon = ctx.renderSortIcon();\n * if (sortIcon) div.appendChild(sortIcon);\n * return div;\n * }\n * ```\n * @since 1.4.0\n */\nexport interface HeaderCellContext<TRow = unknown> {\n /** Column configuration reference. */\n column: ColumnConfig<TRow>;\n /** The header text (from column.header or column.field). */\n value: string;\n /** Current sort state for this column. */\n sortState: 'asc' | 'desc' | null;\n /** Whether the column has an active filter. */\n filterActive: boolean;\n /** The header cell DOM element being rendered into. */\n cellEl: HTMLElement;\n /**\n * Render the standard sort indicator icon.\n * Returns null if column is not sortable.\n */\n renderSortIcon: () => HTMLElement | null;\n /**\n * Render the standard filter button.\n * Returns null if FilteringPlugin is not active or column is not filterable.\n * Note: The actual button is added by FilteringPlugin's afterRender hook.\n */\n renderFilterButton: () => HTMLElement | null;\n}\n\n/**\n * Header label renderer function type.\n * Customize the label while framework handles sort icons, filter buttons, resize handles.\n *\n * Use this for simple label customizations without taking over the entire header.\n * The grid automatically appends sort icons, filter buttons, and resize handles.\n *\n * @example\n * ```typescript\n * // Add required indicator\n * const requiredHeader: HeaderLabelRenderer = (ctx) => {\n * return `${ctx.value} <span style=\"color: red;\">*</span>`;\n * };\n *\n * // Add unit suffix\n * const priceHeader: HeaderLabelRenderer = (ctx) => {\n * const span = document.createElement('span');\n * span.innerHTML = `${ctx.value} <small>(USD)</small>`;\n * return span;\n * };\n *\n * // Column config usage\n * columns: [\n * { field: 'name', headerLabelRenderer: requiredHeader },\n * { field: 'price', headerLabelRenderer: priceHeader },\n * ]\n * ```\n *\n * @see {@link HeaderLabelContext} for context properties\n * @see {@link HeaderRenderer} for full header control\n * @since 1.4.0\n */\nexport type HeaderLabelRenderer<TRow = unknown> = (ctx: HeaderLabelContext<TRow>) => Node | string | void | null;\n\n/**\n * Header cell renderer function type.\n * Full control over header cell content. User is responsible for all content and interactions.\n *\n * When using this, you have complete control but must manually include\n * sort icons, filter buttons, and resize handles using the helper functions.\n *\n * @example\n * ```typescript\n * // Custom header with all standard elements\n * const customHeader: HeaderRenderer = (ctx) => {\n * const div = document.createElement('div');\n * div.className = 'custom-header';\n * div.innerHTML = `<span class=\"label\">${ctx.value}</span>`;\n *\n * // Add sort icon (returns null if not sortable)\n * const sortIcon = ctx.renderSortIcon();\n * if (sortIcon) div.appendChild(sortIcon);\n *\n * // Add filter button (returns null if not filterable)\n * const filterBtn = ctx.renderFilterButton();\n * if (filterBtn) div.appendChild(filterBtn);\n *\n * // Resize handles are added automatically for resizable columns\n * return div;\n * };\n *\n * // Minimal header (no sort/resize)\n * const minimalHeader: HeaderRenderer = (ctx) => {\n * return `<div class=\"minimal\">${ctx.value}</div>`;\n * };\n *\n * // Column config usage\n * columns: [\n * { field: 'name', headerRenderer: customHeader },\n * ]\n * ```\n *\n * @see {@link HeaderCellContext} for context properties and helper functions\n * @see {@link HeaderLabelRenderer} for simpler label-only customization\n * @since 1.4.0\n */\nexport type HeaderRenderer<TRow = unknown> = (ctx: HeaderCellContext<TRow>) => Node | string | void | null;\n// #endregion\n\n// #region Framework Adapter Interface\n/**\n * Framework adapter interface for handling framework-specific component instantiation.\n * Allows framework libraries (Angular, React, Vue) to register handlers that convert\n * declarative light DOM elements into functional renderers/editors.\n *\n * @example\n * ```typescript\n * // In @toolbox-web/grid-angular\n * class AngularGridAdapter implements FrameworkAdapter {\n * canHandle(element: HTMLElement): boolean {\n * return element.tagName.startsWith('APP-');\n * }\n * createRenderer(element: HTMLElement): ColumnViewRenderer {\n * return (ctx) => {\n * // Angular-specific instantiation logic\n * const componentRef = createComponent(...);\n * componentRef.setInput('value', ctx.value);\n * return componentRef.location.nativeElement;\n * };\n * }\n * createEditor(element: HTMLElement): ColumnEditorSpec {\n * return (ctx) => {\n * // Angular-specific editor with commit/cancel\n * const componentRef = createComponent(...);\n * componentRef.setInput('value', ctx.value);\n * // Subscribe to commit/cancel outputs\n * return componentRef.location.nativeElement;\n * };\n * }\n * }\n *\n * // User registers adapter once in their app\n * GridElement.registerAdapter(new AngularGridAdapter(injector, appRef));\n * ```\n * @category Framework Adapters\n * @since 0.2.9\n */\nexport interface FrameworkAdapter {\n /**\n * Determines if this adapter can handle the given element.\n * Typically checks tag name, attributes, or other conventions.\n */\n canHandle(element: HTMLElement): boolean;\n\n /**\n * Creates a view renderer function from a light DOM element.\n * The renderer receives cell context and returns DOM or string.\n * Returns undefined if no renderer template is registered, allowing the grid\n * to use its default rendering.\n */\n createRenderer<TRow = unknown, TValue = unknown>(element: HTMLElement): ColumnViewRenderer<TRow, TValue> | undefined;\n\n /**\n * Creates an editor spec from a light DOM element.\n * The editor receives context with commit/cancel and returns DOM.\n * Returns undefined if no editor template is registered, allowing the grid\n * to use its default built-in editors.\n */\n createEditor<TRow = unknown, TValue = unknown>(element: HTMLElement): ColumnEditorSpec<TRow, TValue> | undefined;\n\n /**\n * Creates a header cell renderer from a light DOM element (e.g. a slot or\n * template on a `<tbw-grid-column>`). Receives `HeaderCellContext` and\n * returns DOM / string for the entire header cell — the user is\n * responsible for sort icons and filter buttons (use\n * `ctx.renderSortIcon()` / `ctx.renderFilterButton()` helpers).\n *\n * Resize handles are appended automatically by the grid for resizable\n * columns regardless of which renderer path is active; do not render one\n * yourself.\n *\n * Returns undefined when the adapter has no header renderer registered\n * for this element, letting the grid fall back to its built-in header.\n *\n * Mirrors `headerRenderer` from `BaseColumnConfig`. Optional on the\n * adapter — adapters that don't expose a slot/template surface for\n * full-header customization can omit it; callers must null-check.\n *\n * @since 2.15.0\n */\n createHeaderRenderer?<TRow = unknown>(element: HTMLElement): HeaderRenderer<TRow> | undefined;\n\n /**\n * Creates a header *label* renderer from a light DOM element. The grid\n * keeps ownership of the sort icon, filter button, and resize handle;\n * the returned function only customizes the label content.\n *\n * Returns undefined when the adapter has no header label renderer\n * registered for this element.\n *\n * Mirrors `headerLabelRenderer` from `BaseColumnConfig`. Optional on\n * the adapter — see `createHeaderRenderer` for rationale.\n *\n * @since 2.15.0\n */\n createHeaderLabelRenderer?<TRow = unknown>(element: HTMLElement): HeaderLabelRenderer<TRow> | undefined;\n\n /**\n * Creates a tool panel renderer from a light DOM element.\n * The renderer receives a container element and optionally returns a cleanup function.\n */\n createToolPanelRenderer?(element: HTMLElement): ((container: HTMLElement) => void | (() => void)) | undefined;\n\n /**\n * Gets type-level defaults from an application-level registry.\n * Used by Angular's `GridTypeRegistry` and React's `GridTypeProvider`.\n *\n * @param type - The column type (e.g., 'date', 'currency', 'country')\n * @param gridEl - The owning `<tbw-grid>` element. Helps adapters resolve\n * the correct context provider in multi-grid scenarios.\n * @returns Type defaults for renderer/editor, or undefined if not registered\n */\n getTypeDefault?<TRow = unknown>(type: string, gridEl?: HTMLElement): TypeDefault<TRow> | undefined;\n\n /**\n * Pre-process a grid config before the grid core applies it.\n * Framework adapters use this to convert framework-specific component references\n * (Angular classes, Vue components, React elements) to DOM-returning functions.\n *\n * Called automatically by the grid's `set gridConfig` setter when a\n * `__frameworkAdapter` is present on the grid instance.\n *\n * Must be **idempotent** — already-processed configs must pass through safely.\n *\n * @param config - The raw grid config (may contain framework-specific values)\n * @returns Processed config with DOM-returning functions\n */\n processConfig?<TRow = unknown>(config: GridConfig<TRow>): GridConfig<TRow>;\n\n /**\n * Called when a cell's content is about to be wiped (e.g., when exiting edit mode,\n * scroll-recycling a row, or rebuilding a row).\n *\n * Framework adapters should use this to properly destroy cached views/components\n * associated with the cell to prevent memory leaks.\n *\n * @param cellEl - The cell element whose content is being released\n */\n releaseCell?(cellEl: HTMLElement): void;\n\n /**\n * Open a teardown batch. Grid core wraps multi-cell teardown sequences\n * (e.g., `_clearRowPool`, row-pool shrink, full row rebuild) where every\n * affected cell will be detached from the DOM before the batch ends.\n *\n * Adapters that normally synchronously commit framework teardown per\n * `releaseCell` (React's `flushSync`) should defer those commits until\n * the matching {@link endBatch} call. Detached containers can then be\n * pruned without emitting per-cell render warnings.\n *\n * Calls may nest; adapters MUST track depth and only flush on the\n * outermost {@link endBatch}.\n *\n * @param gridEl - The grid element whose adapter-managed cells are\n * being torn down. Adapters that key state per grid (e.g. one\n * PortalManager per grid) should scope the batch to this element.\n * Omitted only by callers without a grid reference.\n *\n * @since 2.14.0\n */\n beginBatch?(gridEl?: HTMLElement): void;\n\n /**\n * Close a teardown batch opened by {@link beginBatch}. Adapters should\n * flush any deferred framework commits here (or rely on render-time\n * detached-container filtering for adapters that don't need a flush).\n *\n * @param gridEl - Must match the element passed to the paired\n * {@link beginBatch} call.\n *\n * @since 2.14.0\n */\n endBatch?(gridEl?: HTMLElement): void;\n\n /**\n * Unmount a specific framework container and free its resources.\n *\n * Called by the grid core (e.g., MasterDetailPlugin) when a container\n * created by the adapter is about to be removed from the DOM.\n * The adapter should destroy the associated framework instance\n * (React root, Vue app, Angular view) and remove it from tracking arrays.\n *\n * @param container - The container element returned by a create* method\n */\n unmount?(container: HTMLElement): void;\n\n /**\n * Parse a `<tbw-grid-detail>` element and return a detail renderer function.\n * Used by MasterDetailPlugin to support framework-specific detail templates.\n */\n parseDetailElement?<TRow = unknown>(\n element: Element,\n ): ((row: TRow, rowIndex: number) => HTMLElement | string) | undefined;\n\n /**\n * Parse a `<tbw-grid-responsive-card>` element and return a card renderer function.\n * Used by ResponsivePlugin to support framework-specific card templates.\n */\n parseResponsiveCardElement?<TRow = unknown>(\n element: Element,\n ): ((row: TRow, rowIndex: number) => HTMLElement) | undefined;\n}\n// #endregion\n\n// #region Internal Types\n\n/**\n * Extended column config used internally.\n * Includes all internal properties needed during grid lifecycle.\n *\n * Plugin developers may need to access these when working with\n * column caching and compiled templates.\n *\n * @example\n * ```typescript\n * import type { ColumnInternal } from '@toolbox-web/grid';\n *\n * class MyPlugin extends BaseGridPlugin {\n * afterRender(): void {\n * // Access internal column properties\n * const columns = this.columns as ColumnInternal[];\n * for (const col of columns) {\n * // Check if column was auto-sized\n * if (col.__autoSized) {\n * console.log(`${col.field} was auto-sized`);\n * }\n * }\n * }\n * }\n * ```\n *\n * @see {@link ColumnConfig} for public column properties\n * @category Plugin Development\n * @internal\n * @since 0.1.1\n */\nexport interface ColumnInternal<T = any> extends ColumnConfig<T> {\n __autoSized?: boolean;\n __userResized?: boolean;\n __renderedWidth?: number;\n /** Original configured width (for reset on double-click) */\n __originalWidth?: number;\n __viewTemplate?: HTMLElement;\n __editorTemplate?: HTMLElement;\n __headerTemplate?: HTMLElement;\n __compiledView?: CompiledViewFunction<T>;\n __compiledEditor?: (ctx: EditorExecContext<T>) => string;\n /**\n * The originating `<tbw-grid-column>` light-DOM element, if this column was\n * parsed from declarative HTML. Plugins read their own attributes from this\n * element inside their `processColumns` hook (see issue #272). Core only\n * parses a minimal set of structural attributes; everything else is\n * plugin-owned.\n * @since 2.16.0\n */\n __element?: HTMLElement;\n}\n\n/**\n * Row element with internal tracking properties.\n * Used during virtualization and row pooling.\n *\n * @category Plugin Development\n * @internal\n * @since 0.4.0\n */\nexport interface RowElementInternal extends HTMLElement {\n /** Epoch marker for row render invalidation */\n __epoch?: number;\n /** Reference to the row data object for change detection */\n __rowDataRef?: unknown;\n /** Count of cells currently in edit mode */\n __editingCellCount?: number;\n /** Flag indicating this is a custom-rendered row (group row, etc.) */\n __isCustomRow?: boolean;\n}\n\n/**\n * Type-safe access to element.part API (DOMTokenList-like).\n * Used for CSS ::part styling support.\n * @internal\n */\nexport interface ElementWithPart {\n part?: DOMTokenList;\n}\n\n/**\n * Compiled view function type with optional blocked flag.\n * The __blocked flag is set when a template contains unsafe expressions.\n *\n * @category Plugin Development\n * @internal\n * @since 0.4.0\n */\nexport interface CompiledViewFunction<T = any> {\n (ctx: CellContext<T>): string;\n /** Set to true when template was blocked due to unsafe expressions */\n __blocked?: boolean;\n}\n\n/**\n * Runtime cell context used internally for compiled template execution.\n *\n * Contains the minimal context needed to render a cell: the row data,\n * cell value, field name, and column configuration.\n *\n * @example\n * ```typescript\n * import type { CellContext, ColumnInternal } from '@toolbox-web/grid';\n *\n * // Used internally by compiled templates\n * const renderCell = (ctx: CellContext) => {\n * return `<span title=\"${ctx.field}\">${ctx.value}</span>`;\n * };\n * ```\n *\n * @see {@link CellRenderContext} for public cell render context\n * @see {@link EditorExecContext} for editor context with commit/cancel\n * @category Plugin Development\n * @since 0.1.1\n */\nexport interface CellContext<T = any> {\n row: T;\n value: unknown;\n field: string;\n column: ColumnInternal<T>;\n /** Optional template context for declarative <tbw-grid-type> defaults. */\n typeDefault?: Record<string, unknown> | null;\n}\n\n/**\n * Internal editor execution context extending the generic cell context with commit helpers.\n *\n * Used internally by the editing system. For public editor APIs,\n * prefer using {@link ColumnEditorContext}.\n *\n * @example\n * ```typescript\n * import type { EditorExecContext } from '@toolbox-web/grid';\n *\n * // Internal editor template execution\n * const execEditor = (ctx: EditorExecContext) => {\n * const input = document.createElement('input');\n * input.value = String(ctx.value);\n * input.onkeydown = (e) => {\n * if (e.key === 'Enter') ctx.commit(input.value);\n * if (e.key === 'Escape') ctx.cancel();\n * };\n * return input;\n * };\n * ```\n *\n * @see {@link ColumnEditorContext} for public editor context\n * @see {@link CellContext} for base cell context\n * @category Plugin Development\n * @since 0.1.1\n */\nexport interface EditorExecContext<T = any> extends CellContext<T> {\n commit: (newValue: unknown) => void;\n cancel: () => void;\n}\n\n/**\n * Controller managing drag-based column resize lifecycle.\n *\n * Exposed internally for plugins that need to interact with resize behavior.\n *\n * @example\n * ```typescript\n * import type { ResizeController, InternalGrid } from '@toolbox-web/grid';\n *\n * class MyPlugin extends BaseGridPlugin {\n * handleColumnAction(colIndex: number): void {\n * const grid = this.grid as InternalGrid;\n * const resizeCtrl = grid._resizeController;\n *\n * // Check if resize is in progress\n * if (resizeCtrl?.isResizing) {\n * return; // Don't interfere\n * }\n *\n * // Reset column to configured width\n * resizeCtrl?.resetColumn(colIndex);\n * }\n * }\n * ```\n *\n * @see {@link ColumnResizeDetail} for resize event details\n * @category Plugin Development\n * @since 0.1.1\n */\nexport interface ResizeController {\n start: (e: MouseEvent, colIndex: number, cell: HTMLElement) => void;\n /** Reset a column to its configured width (or auto-size if none configured). */\n resetColumn: (colIndex: number) => void;\n dispose: () => void;\n /** True while a resize drag is in progress (used to suppress header click/sort). */\n isResizing: boolean;\n}\n\n/**\n * Virtual window bookkeeping; modified in-place as scroll position changes.\n *\n * Tracks virtualization state for row rendering. The grid only renders\n * rows within the visible viewport window (start to end) plus overscan.\n *\n * @example\n * ```typescript\n * import type { VirtualState, InternalGrid } from '@toolbox-web/grid';\n *\n * class MyPlugin extends BaseGridPlugin {\n * logVirtualWindow(): void {\n * const grid = this.grid as InternalGrid;\n * const vs = grid.virtualization;\n *\n * console.log(`Row height: ${vs.rowHeight}px`);\n * console.log(`Visible rows: ${vs.start} to ${vs.end}`);\n * console.log(`Virtualization: ${vs.enabled ? 'on' : 'off'}`);\n * }\n * }\n * ```\n *\n * @see {@link GridConfig.rowHeight} for configuring row height\n * @category Plugin Development\n * @since 0.1.1\n */\nexport interface VirtualState {\n enabled: boolean;\n rowHeight: number;\n /** Threshold for bypassing virtualization (renders all rows if totalRows <= bypassThreshold) */\n bypassThreshold: number;\n start: number;\n end: number;\n /** Faux scrollbar element that provides scroll events (AG Grid pattern) */\n container: HTMLElement | null;\n /** Rows viewport element for measuring visible area height */\n viewportEl: HTMLElement | null;\n /** Spacer element inside faux scrollbar for setting virtual height */\n totalHeightEl: HTMLElement | null;\n\n // --- Variable Row Height Support (Phase 1) ---\n\n /**\n * Position cache for variable row heights.\n * Index-based array mapping row index → {offset, height, measured}.\n * Rebuilt when row count changes (expand/collapse, filter).\n * `null` when using uniform row heights (default).\n */\n positionCache: RowPositionEntry[] | null;\n\n /**\n * Height cache by row identity.\n * Persists row heights across position cache rebuilds.\n * Uses dual storage: Map for string keys (rowId, __rowCacheKey) and WeakMap for object refs.\n */\n heightCache: {\n /** Heights keyed by string (synthetic rows with __rowCacheKey, or rowId-keyed rows) */\n byKey: Map<string, number>;\n /** Heights keyed by object reference (data rows without rowId) */\n byRef: WeakMap<object, number>;\n };\n\n /** Running average of measured row heights. Used for estimating unmeasured rows. */\n averageHeight: number;\n\n /** Number of rows that have been measured. */\n measuredCount: number;\n\n /** Whether variable row height mode is active (rowHeight is a function). */\n variableHeights: boolean;\n\n // --- Cached Geometry (avoid forced layout reads on scroll hot path) ---\n\n /** Cached viewport element height. Updated by ResizeObserver and force-refresh only. */\n cachedViewportHeight: number;\n\n /** Cached faux scrollbar element height. Updated alongside viewport height. */\n cachedFauxHeight: number;\n\n /** Cached scroll-area element height. Updated alongside viewport/faux heights. */\n cachedScrollAreaHeight: number;\n\n /** Cached reference to .tbw-scroll-area element. Set during scroll listener setup. */\n scrollAreaEl: HTMLElement | null;\n\n /**\n * Active scroll mapping between native `scrollTop` (clamped spacer space) and\n * \"virtual\" row-content space. Identity (`capped: false`) for datasets within\n * the browser's max-element-height cap (Chromium ~33.5M px). For larger datasets,\n * the spacer height is clamped and `scrollTop` must be translated via this\n * mapping before computing the visible window. Updated by `calculateTotalSpacerHeight`.\n *\n * @see {@link computeScrollMapping}\n * @since 2.13.0\n */\n scrollMapping: ScrollMapping;\n}\n\n// RowElementInternal is now defined earlier in the file with all internal properties\n\n/**\n * Union type for input-like elements that have a `value` property.\n * Covers standard form elements and custom elements with value semantics.\n *\n * @category Plugin Development\n * @internal\n * @since 0.4.0\n */\nexport type InputLikeElement = HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement | { value: unknown };\n// #endregion\n\n// #region Grouping & Footer Public Types\n/**\n * Group row rendering customization options.\n * Controls how group header rows are displayed in the GroupingRowsPlugin.\n *\n * @example\n * ```typescript\n * import { GroupingRowsPlugin } from '@toolbox-web/grid/all';\n *\n * new GroupingRowsPlugin({\n * groupOn: (row) => [row.department, row.team],\n * render: {\n * // Group row spans all columns\n * fullWidth: true,\n *\n * // Custom label format\n * formatLabel: (value, depth, key) => {\n * if (depth === 0) return `Department: ${value}`;\n * return `Team: ${value}`;\n * },\n *\n * // Show aggregates in group rows (when not fullWidth)\n * aggregators: {\n * salary: 'sum',\n * age: 'avg',\n * },\n *\n * // Custom CSS class\n * class: 'my-group-row',\n * },\n * });\n * ```\n *\n * @see {@link AggregatorRef} for aggregation options\n * @since 0.1.1\n */\nexport interface RowGroupRenderConfig {\n /** If true, group rows span all columns (single full-width cell). Default false. */\n fullWidth?: boolean;\n /** Optional label formatter override. Receives raw group value + depth. */\n formatLabel?: (value: unknown, depth: number, key: string) => string;\n /** Optional aggregate overrides per field for group summary cells (only when not fullWidth). */\n aggregators?: Record<string, AggregatorRef>;\n /** Additional CSS class applied to each group row root element. */\n class?: string;\n}\n\n/**\n * Reference to an aggregation function for footer/group summaries.\n *\n * Can be either:\n * - A built-in aggregator name: `'sum'`, `'avg'`, `'min'`, `'max'`, `'count'`\n * - A custom function that calculates the aggregate value\n *\n * Aggregators are not declared on a column directly — they are configured per\n * field on the consumer (e.g. {@link RowGroupRenderConfig.aggregators} for\n * group rows, keyed by column field name).\n *\n * @example\n * ```typescript\n * // Built-in aggregator on a group row config\n * { aggregators: { amount: 'sum' } }\n *\n * // Custom aggregator function\n * {\n * aggregators: {\n * price: (rows, field) => {\n * const values = rows.map((r) => (r as any)[field]).filter((v) => v != null);\n * return values.length ? Math.max(...values as number[]) : null;\n * },\n * },\n * }\n * ```\n *\n * @see {@link RowGroupRenderConfig} for using aggregators in group rows\n * @since 0.1.1\n */\nexport type AggregatorRef = string | ((rows: unknown[], field: string, column?: unknown) => unknown);\n\n/**\n * Result of automatic column inference from sample rows.\n *\n * When no columns are configured, the grid analyzes the first row of data\n * to automatically generate column definitions with inferred types.\n *\n * @example\n * ```typescript\n * // Automatic inference (no columns configured)\n * grid.rows = [\n * { name: 'Alice', age: 30, active: true, hireDate: new Date() },\n * ];\n * // Grid infers:\n * // - name: type 'string'\n * // - age: type 'number'\n * // - active: type 'boolean'\n * // - hireDate: type 'date'\n *\n * // Access inferred result programmatically\n * const config = await grid.getConfig();\n * console.log(config.columns); // Inferred columns\n * ```\n *\n * @see {@link ColumnConfig} for column configuration options\n * @see {@link ColumnType} for type inference rules\n * @since 0.1.1\n */\nexport interface InferredColumnResult<TRow = unknown> {\n /** Generated column configurations based on data analysis */\n columns: ColumnConfigMap<TRow>;\n /** Map of field names to their inferred types */\n typeMap: Record<string, ColumnType>;\n}\n\n/**\n * Column sizing mode.\n *\n * - `'fixed'` - Columns use their configured widths. Horizontal scrolling if content overflows.\n * - `'stretch'` - Columns stretch proportionally to fill available width. No horizontal scrolling.\n *\n * @example\n * ```typescript\n * // Fixed widths - good for many columns\n * grid.fitMode = 'fixed';\n *\n * // Stretch to fill - good for few columns\n * grid.fitMode = 'stretch';\n *\n * // Via gridConfig\n * grid.gridConfig = { fitMode: 'stretch' };\n * ```\n * @since 0.1.1\n */\nexport const FitModeEnum = {\n STRETCH: 'stretch',\n FIXED: 'fixed',\n} as const;\n/**\n * Column sizing mode — determines how columns fill the available grid width.\n * Use `FitModeEnum` to access individual values by key.\n * @since 0.1.1\n */\nexport type FitMode = (typeof FitModeEnum)[keyof typeof FitModeEnum];\n\n/**\n * How automatic column inference combines with explicitly provided columns.\n *\n * - `'auto'` (default): infer columns only when none are provided. Declaring a\n * single column (via `columns`, `gridConfig.columns`, or `<tbw-grid-column>`)\n * disables inference and renders only the declared column(s).\n * - `'merge'`: always infer the full column set from the data (in data-key\n * order), then overlay any explicitly provided columns matched by `field`.\n * A provided column customizes only its own field and keeps its data\n * position; provided columns for fields absent from the data are appended as\n * computed columns.\n *\n * Use `ColumnInferenceModeEnum` to access individual values by key.\n * @since 2.17.0\n */\nexport const ColumnInferenceModeEnum = {\n AUTO: 'auto',\n MERGE: 'merge',\n} as const;\n/**\n * Column inference mode — see {@link ColumnInferenceModeEnum} for values.\n * @since 2.17.0\n */\nexport type ColumnInferenceMode = (typeof ColumnInferenceModeEnum)[keyof typeof ColumnInferenceModeEnum];\n// #endregion\n\n// #region Plugin Interface\n/**\n * Minimal plugin interface for type-checking.\n * This interface is defined here to avoid circular imports with BaseGridPlugin.\n * All plugins must satisfy this shape (BaseGridPlugin implements it).\n *\n * @example\n * ```typescript\n * // Using plugins in grid config\n * import { SelectionPlugin, FilteringPlugin } from '@toolbox-web/grid/all';\n *\n * grid.gridConfig = {\n * plugins: [\n * new SelectionPlugin({ mode: 'row' }),\n * new FilteringPlugin({ debounceMs: 200 }),\n * ],\n * };\n *\n * // Accessing plugin instance at runtime (preferred)\n * const selection = grid.getPluginByName('selection');\n * if (selection) {\n * selection.selectAll();\n * }\n * ```\n *\n * @category Plugin Development\n * @since 0.2.3\n */\nexport interface GridPlugin {\n /** Unique plugin identifier */\n readonly name: string;\n /** Plugin version */\n readonly version: string;\n /** CSS styles to inject into the grid */\n readonly styles?: string;\n}\n\n/**\n * Plugin name-to-type registry for type-safe `getPluginByName()`.\n *\n * Plugins augment this interface via `declare module` so that\n * `grid.getPluginByName('editing')` returns `EditingPlugin | undefined`\n * instead of `GridPlugin | undefined`.\n *\n * @example\n * ```typescript\n * // Plugin augmentation (done automatically when you import a plugin):\n * declare module '../../core/types' {\n * interface PluginNameMap {\n * editing: EditingPlugin;\n * }\n * }\n *\n * // Consumer usage — fully typed:\n * const editing = grid.getPluginByName('editing');\n * editing?.beginBulkEdit(0); // ✅ No cast needed\n * ```\n *\n * @category Plugin Development\n */\n// eslint-disable-next-line @typescript-eslint/no-empty-object-type, @typescript-eslint/no-empty-interface\nexport interface PluginNameMap {}\n// #endregion\n\n// #region Feature Config\n/**\n * Declarative feature configuration interface.\n *\n * This interface is intentionally empty in core — it is populated via **module augmentation**\n * by feature side-effect imports (`@toolbox-web/grid/features/selection`, etc.).\n * Third-party plugins can also augment this interface to add their own features.\n *\n * @example\n * ```ts\n * // Each feature import augments this interface:\n * import '@toolbox-web/grid/features/selection';\n * import '@toolbox-web/grid/features/filtering';\n *\n * grid.gridConfig = {\n * features: {\n * selection: 'range', // ← typed by selection feature module\n * filtering: { debounceMs: 200 }, // ← typed by filtering feature module\n * },\n * };\n * ```\n *\n * @example Third-party augmentation\n * ```ts\n * declare module '@toolbox-web/grid' {\n * interface FeatureConfig {\n * sparkline?: boolean | SparklineConfig;\n * }\n * }\n * ```\n * @since 1.24.0\n */\n// eslint-disable-next-line @typescript-eslint/no-unused-vars -- TRow must keep this exact name so module augmentations across features merge (TS2428).\nexport interface FeatureConfig<TRow = unknown> {\n /**\n * @internal Sentinel property that makes the interface non-empty so TypeScript's\n * excess-property checking rejects unknown feature keys in object literals.\n * Not assignable at runtime (type is `never`).\n */\n __brand?: never;\n}\n// #endregion\n\n// #region Accessibility Config\n\n/**\n * Default announcement messages for screen reader live regions.\n * Each function returns a localized string for a specific state change.\n * Override individual messages via {@link A11yConfig.messages} for i18n.\n *\n * @group Accessibility\n * @since 2.0.0\n */\nexport interface A11yMessages {\n /** Announced when sorting is applied. */\n sortApplied: (column: string, direction: string) => string;\n /** Announced when sorting is cleared. */\n sortCleared: () => string;\n /** Announced when a filter is applied. */\n filterApplied: (column: string) => string;\n /** Announced when a filter is cleared from a column. */\n filterCleared: (column: string) => string;\n /** Announced when all filters are cleared. */\n allFiltersCleared: () => string;\n /** Announced when a group row is expanded. */\n groupExpanded: (name: string, count: number) => string;\n /** Announced when a group row is collapsed. */\n groupCollapsed: (name: string) => string;\n /** Announced when row selection changes. */\n selectionChanged: (count: number) => string;\n /** Announced when a single column is selected. @since 2.8.0 */\n columnSelected: (label: string) => string;\n /** Announced when multiple columns are selected. @since 2.8.0 */\n columnSelectionChanged: (count: number) => string;\n /** Announced when column selection is cleared. @since 2.8.0 */\n columnSelectionCleared: () => string;\n /** Announced when the active selection axis flips between row and column. @since 2.8.0 */\n selectionAxisChanged: (toAxis: 'row' | 'column') => string;\n /** Announced when row editing starts. */\n editingStarted: (rowIndex: number) => string;\n /** Announced when row editing is committed. */\n editingCommitted: (rowIndex: number) => string;\n /** Announced when data is loaded. */\n dataLoaded: (count: number) => string;\n}\n\n/**\n * Accessibility configuration for controlling screen reader announcements.\n *\n * @group Accessibility\n *\n * @example\n * ```ts\n * // Disable all live announcements\n * a11y: { announcements: false }\n *\n * // Override specific messages for French locale\n * a11y: {\n * messages: {\n * sortApplied: (col, dir) => `Trié par ${col}, ${dir}`,\n * sortCleared: () => 'Tri effacé',\n * },\n * }\n * ```\n * @since 2.0.0\n */\nexport interface A11yConfig {\n /**\n * Enable or disable live region announcements.\n * When `false`, the `aria-live` region remains in the DOM but no messages are set.\n * @defaultValue `true`\n */\n announcements?: boolean;\n /**\n * Custom announcement text overrides for internationalization.\n * Partial — only override the messages you need; defaults are used for the rest.\n */\n messages?: Partial<A11yMessages>;\n}\n\n/**\n * Default English announcement messages.\n * Used when no custom messages are provided via {@link A11yConfig.messages}.\n * @since 2.0.0\n */\nexport const DEFAULT_A11Y_MESSAGES: A11yMessages = {\n sortApplied: (column, direction) => `Sorted by ${column}, ${direction}`,\n sortCleared: () => 'Sort cleared',\n filterApplied: (column) => `Filter applied on ${column}`,\n filterCleared: (column) => `Filter cleared from ${column}`,\n allFiltersCleared: () => 'All filters cleared',\n groupExpanded: (name, count) => `Group ${name} expanded, ${count} rows`,\n groupCollapsed: (name) => `Group ${name} collapsed`,\n selectionChanged: (count) => `${count} rows selected`,\n columnSelected: (label) => `Column ${label} selected`,\n columnSelectionChanged: (count) => `${count} columns selected`,\n columnSelectionCleared: () => 'Column selection cleared',\n selectionAxisChanged: (toAxis) =>\n toAxis === 'column'\n ? 'Row selection cleared, column selection active'\n : 'Column selection cleared, row selection active',\n editingStarted: (rowIndex) => `Editing row ${rowIndex + 1}`,\n editingCommitted: (rowIndex) => `Row ${rowIndex + 1} saved`,\n dataLoaded: (count) => `${count} rows loaded`,\n};\n\n// #endregion\n\n// #region Grid Config\n/**\n * Grid configuration object - the **single source of truth** for grid behavior.\n *\n * Users can configure the grid via multiple input methods, all of which converge\n * into an effective `GridConfig` internally:\n *\n * **Configuration Input Methods:**\n * - `gridConfig` property - direct assignment of this object\n * - `columns` property - shorthand for `gridConfig.columns`\n * - `fitMode` property - shorthand for `gridConfig.fitMode`\n * - Light DOM `<tbw-grid-column>` - declarative columns (merged into `columns`)\n * - Light DOM `<tbw-grid-header>` - declarative shell header (merged into `shell.header`)\n *\n * **Precedence (when same property set multiple ways):**\n * Individual props (`fitMode`) > `columns` prop > Light DOM > `gridConfig`\n *\n * @example\n * ```ts\n * // Via gridConfig (recommended for complex setups)\n * grid.gridConfig = {\n * columns: [{ field: 'name' }, { field: 'age' }],\n * fitMode: 'stretch',\n * plugins: [new SelectionPlugin()],\n * shell: { header: { title: 'My Grid' } }\n * };\n *\n * // Via individual props (convenience for simple cases)\n * grid.columns = [{ field: 'name' }, { field: 'age' }];\n * grid.fitMode = 'stretch';\n * ```\n * @since 0.1.1\n */\nexport interface GridConfig<TRow = any> {\n /**\n * Column definitions. Can also be set via `columns` prop or `<tbw-grid-column>` light DOM.\n * @see {@link ColumnConfig} for column options\n * @see {@link ColumnConfigMap}\n */\n columns?: ColumnConfigMap<TRow>;\n /**\n * Dynamic CSS class(es) for data rows.\n * Called for each row during rendering. Return class names to add to the row element.\n *\n * @example\n * ```typescript\n * // Highlight inactive rows\n * rowClass: (row) => row.active ? [] : ['inactive', 'dimmed']\n *\n * // Status-based row styling\n * rowClass: (row) => [`priority-${row.priority}`]\n *\n * // Single class as string\n * rowClass: (row) => row.isNew ? 'new-row' : ''\n * ```\n */\n rowClass?: (row: TRow) => string | string[];\n /** Sizing mode for columns. Can also be set via `fitMode` prop. */\n fitMode?: FitMode;\n\n /**\n * How automatic column inference combines with explicitly provided columns.\n * Can also be set via the `columnInference` prop or `column-inference` attribute.\n *\n * - `'auto'` (default): infer only when no columns are provided (current behavior).\n * - `'merge'`: always infer from data, then overlay provided columns by `field`.\n *\n * @see {@link ColumnInferenceMode}\n * @since 2.17.0\n */\n columnInference?: ColumnInferenceMode;\n /**\n * Grid-wide sorting toggle.\n * When false, disables sorting for all columns regardless of their individual `sortable` setting.\n * When true (default), columns with `sortable: true` can be sorted.\n *\n * This affects:\n * - Header click handlers for sorting\n * - Sort indicator visibility\n * - Multi-sort plugin behavior (if loaded)\n *\n * @default true\n *\n * @example\n * ```typescript\n * // Disable all sorting\n * gridConfig = { sortable: false };\n *\n * // Enable sorting (default) - individual columns still need sortable: true\n * gridConfig = { sortable: true };\n * ```\n */\n sortable?: boolean;\n\n /**\n * Grid-wide resizing toggle.\n * When false, disables column resizing for all columns regardless of their individual `resizable` setting.\n * When true (default), columns with `resizable: true` (or resizable not set, since it defaults to true) can be resized.\n *\n * This affects:\n * - Resize handle visibility in header cells\n * - Double-click to auto-size behavior\n *\n * @default true\n *\n * @example\n * ```typescript\n * // Disable all column resizing\n * gridConfig = { resizable: false };\n *\n * // Enable resizing (default) - individual columns can opt out with resizable: false\n * gridConfig = { resizable: true };\n * ```\n */\n resizable?: boolean;\n\n /**\n * Row height in pixels for virtualization calculations.\n * The virtualization system assumes uniform row heights for performance.\n *\n * If not specified, the grid measures the first rendered row's height,\n * which respects the CSS variable `--tbw-row-height` set by themes.\n *\n * Set this explicitly when:\n * - Row content may wrap to multiple lines (also set `--tbw-cell-white-space: normal`)\n * - Using custom row templates with variable content\n * - You want to override theme-defined row height\n * - Rows have different heights based on content (use function form)\n *\n * **Variable Row Heights**: When a function is provided, the grid enables variable height\n * virtualization. Heights are measured on first render and cached by row identity.\n *\n * @default Auto-measured from first row (respects --tbw-row-height CSS variable)\n *\n * @example\n * ```ts\n * // Fixed height for all rows\n * gridConfig = { rowHeight: 56 };\n *\n * // Variable height based on content\n * gridConfig = {\n * rowHeight: (row, index) => row.hasDetails ? 80 : 40,\n * };\n *\n * // Return undefined to trigger DOM auto-measurement\n * gridConfig = {\n * rowHeight: (row) => row.isExpanded ? undefined : 40,\n * };\n * ```\n */\n rowHeight?: number | ((row: TRow, index: number) => number | undefined);\n /**\n * Array of plugin instances.\n * Each plugin is instantiated with its configuration and attached to this grid.\n *\n * @example\n * ```ts\n * plugins: [\n * new SelectionPlugin({ mode: 'range' }),\n * new MultiSortPlugin(),\n * new FilteringPlugin({ debounceMs: 150 }),\n * ]\n * ```\n */\n plugins?: GridPlugin[];\n\n /**\n * Declarative feature configuration.\n * Alternative to manually creating plugin instances in `plugins`.\n * Features are resolved using the core feature registry.\n *\n * Import feature modules as side effects to register them:\n * ```ts\n * import '@toolbox-web/grid/features/selection';\n * import '@toolbox-web/grid/features/filtering';\n * ```\n *\n * Then configure declaratively:\n * ```ts\n * gridConfig = {\n * features: {\n * selection: 'range',\n * filtering: { debounceMs: 200 },\n * editing: 'dblclick',\n * },\n * };\n * ```\n *\n * Both `features` and `plugins` can be used together — features-generated plugins\n * are created first, then manual `plugins` are appended. Duplicates are skipped\n * (manual `plugins` take precedence).\n */\n features?: Partial<FeatureConfig<TRow>>;\n\n /**\n * Saved column state to restore on initialization.\n * Includes order, width, visibility, sort, and plugin-contributed state.\n */\n columnState?: GridColumnState;\n\n // NOTE: `shell?: ShellConfig` is contributed via module augmentation from\n // `plugins/shell/types.ts` (extraction #370). Core has no intrinsic shell field.\n\n /**\n * Grid-wide icon configuration.\n *\n * The grid uses a **CSS-first hybrid icon system**:\n * - **Default (CSS):** Icons render via `--tbw-icon-*` CSS custom properties on `tbw-grid`.\n * Override them in your theme CSS — no JavaScript needed.\n * - **JS override:** Setting `gridConfig.icons` takes precedence over CSS for any key provided.\n * Use this for dynamic icons, icon libraries, or `HTMLElement` instances.\n *\n * All icons are optional — sensible defaults are used when not specified.\n * Plugins will use these by default but can override with their own config.\n */\n icons?: GridIcons;\n\n /**\n * Grid-wide animation configuration.\n * Controls animations for expand/collapse, reordering, and other visual transitions.\n * Individual plugins can override these defaults in their own config.\n */\n animation?: AnimationConfig;\n\n /**\n * Custom sort handler for the entire grid.\n *\n * :::caution\n * **Prefer {@link BaseColumnConfig.sortComparator} over `sortHandler`.**\n *\n * `sortHandler` is a low-level escape hatch with significant limitations:\n * - Only consulted by the **single-column** sort path (core header click,\n * `TreePlugin` per-level sort, `ServerSidePlugin` `sortMode: 'local'`).\n * - **Bypassed entirely** when `MultiSortPlugin` is loaded.\n * - Owns ALL columns at once — your handler must implement field/direction\n * dispatch and null handling for every sortable column itself.\n *\n * For per-column custom sort logic, use {@link BaseColumnConfig.sortComparator}\n * instead. It is honored by every sort code path in the grid (core, multi-sort,\n * tree, server-side) and is composable across columns.\n *\n * For server-side sort, prefer `ServerSideConfig.dataSource` — the\n * `sortModel` is shipped to your `getRows` handler so the backend can return\n * pre-sorted blocks.\n * :::\n *\n * Use `sortHandler` only when you need to replace the grid's sort engine\n * wholesale (e.g. integrating a third-party sort library that operates on\n * the full row array, or routing every sort through a single async pipeline).\n *\n * The handler receives:\n * - `rows`: Current row array to sort\n * - `sortState`: Sort field and direction (1 = asc, -1 = desc)\n * - `columns`: Column configurations (for accessing sortComparator)\n *\n * Return the sorted array (sync) or a Promise that resolves to it (async).\n *\n * @example\n * ```ts\n * // Replace the entire client-side sort engine with a custom stable sort\n * sortHandler: (rows, state) => stableSort(rows, state.field, state.direction);\n * ```\n *\n * @see {@link BaseColumnConfig.sortComparator} — recommended per-column override\n * @see `ServerSideConfig.dataSource` — recommended server-side sort path\n */\n sortHandler?: SortHandler<TRow>;\n\n /**\n * Initial sort state applied when the grid first renders.\n *\n * Equivalent to calling `grid.sort(field, direction)` after the grid is created,\n * but avoids the imperative call and extra render cycle.\n *\n * @example\n * ```ts\n * gridConfig = {\n * initialSort: { field: 'salary', direction: 'desc' },\n * };\n * ```\n *\n * @see {@link DataGridElement.sort} for runtime sorting\n * @see {@link DataGridElement.sortModel} for reading current sort state\n */\n initialSort?: { field: string; direction: 'asc' | 'desc' };\n\n /**\n * Function to extract a unique identifier from a row.\n * Used by `updateRow()`, `getRow()`, and ID-based tracking.\n *\n * If not provided, falls back to `row.id` or `row._id` if present.\n * Rows without IDs are silently skipped during map building.\n * Only throws when explicitly calling `getRowId()` or `updateRow()` on a row without an ID.\n *\n * @example\n * ```ts\n * // Simple field\n * getRowId: (row) => row.id\n *\n * // Composite key\n * getRowId: (row) => `${row.voyageId}-${row.legNumber}`\n *\n * // UUID field\n * getRowId: (row) => row.uuid\n * ```\n */\n getRowId?: (row: TRow) => string;\n\n /**\n * Type-level renderer and editor defaults.\n *\n * Keys can be:\n * - Built-in types: `'string'`, `'number'`, `'date'`, `'boolean'`, `'select'`\n * - Custom types: `'currency'`, `'country'`, `'status'`, etc.\n *\n * Resolution order (highest priority first):\n * 1. Column-level (`column.renderer` / `column.editor`)\n * 2. Grid-level (`gridConfig.typeDefaults[column.type]`)\n * 3. App-level (Angular `GridTypeRegistry`, React `GridTypeProvider`)\n * 4. Built-in (checkbox for boolean, select for select, etc.)\n * 5. Fallback (plain text / text input)\n *\n * @example\n * ```typescript\n * typeDefaults: {\n * date: { editor: myDatePickerEditor },\n * country: {\n * renderer: (ctx) => {\n * const span = document.createElement('span');\n * span.innerHTML = `<img src=\"/flags/${ctx.value}.svg\" /> ${ctx.value}`;\n * return span;\n * },\n * editor: (ctx) => createCountrySelect(ctx)\n * }\n * }\n * ```\n */\n typeDefaults?: Record<string, TypeDefault<TRow>>;\n\n // #region Accessibility\n\n /**\n * Accessible label for the grid.\n * Sets `aria-label` on the grid's internal table element for screen readers.\n *\n * If not provided and `shell.header.title` is set, the title is used automatically.\n *\n * If [`gridAriaLabelledBy`](#gridarialabelledby) is also set, `aria-labelledby`\n * takes precedence per WAI-ARIA accessible-name computation and `aria-label`\n * is omitted.\n *\n * @example\n * ```ts\n * gridConfig = { gridAriaLabel: 'Employee data' };\n * ```\n */\n gridAriaLabel?: string;\n\n /**\n * ID of an element that labels the grid.\n * Sets `aria-labelledby` on the grid's internal table element so screen\n * readers can use the referenced element's text as the accessible name —\n * useful when the grid already sits next to a heading.\n *\n * Per WAI-ARIA accessible-name precedence, `aria-labelledby` takes priority\n * over `aria-label` and over the auto-derived shell title. When this option\n * is set, the grid omits `aria-label` to avoid conflicting names.\n *\n * @example\n * ```html\n * <h2 id=\"grid-heading\">Employees</h2>\n * <tbw-grid></tbw-grid>\n * ```\n * ```ts\n * gridConfig = { gridAriaLabelledBy: 'grid-heading' };\n * ```\n */\n gridAriaLabelledBy?: string;\n\n /**\n * ID of an element that describes the grid.\n * Sets `aria-describedby` on the grid's internal table element.\n *\n * @example\n * ```html\n * <p id=\"grid-desc\">This table shows all active employees.</p>\n * <tbw-grid></tbw-grid>\n * ```\n * ```ts\n * gridConfig = { gridAriaDescribedBy: 'grid-desc' };\n * ```\n */\n gridAriaDescribedBy?: string;\n\n /**\n * Override the screen-reader-announced role name for the grid via\n * `aria-roledescription`. Useful for localization (e.g. `\"Tabell\"` in\n * Norwegian) or domain-specific naming (e.g. `\"Employee table\"`).\n *\n * :::caution\n * Per [WAI-ARIA 1.2](https://www.w3.org/TR/wai-aria-1.2/#aria-roledescription),\n * the value should still describe a grid-like widget. Overriding with an\n * unrelated label confuses assistive-technology users about the available\n * interactions (cell navigation, sort, etc.). Leave unset to use the\n * default role name announced by the AT.\n * :::\n *\n * @example\n * ```ts\n * gridConfig = { gridAriaRoleDescription: 'Employee table' };\n * ```\n */\n gridAriaRoleDescription?: string;\n\n /**\n * Accessibility configuration for screen reader announcements.\n *\n * The grid automatically announces state changes (sort, filter, selection, etc.)\n * via an `aria-live` region. Use this config to toggle announcements or override\n * message text for internationalization.\n *\n * @example\n * ```ts\n * // Disable all announcements\n * gridConfig = { a11y: { announcements: false } };\n *\n * // Custom messages for i18n\n * gridConfig = {\n * a11y: {\n * messages: {\n * sortApplied: (col, dir) => `Trié par ${col}, ${dir}`,\n * filterApplied: (col) => `Filtre appliqué sur ${col}`,\n * },\n * },\n * };\n * ```\n */\n a11y?: A11yConfig;\n\n // #endregion\n\n // #region Loading\n\n /**\n * Custom renderer for the loading overlay.\n *\n * When provided, replaces the default spinner with custom content.\n * Receives a context object with the current loading size.\n *\n * @example\n * ```typescript\n * // Simple text loading indicator\n * loadingRenderer: () => {\n * const el = document.createElement('div');\n * el.textContent = 'Loading...';\n * return el;\n * }\n *\n * // Custom spinner component\n * loadingRenderer: (ctx) => {\n * const spinner = document.createElement('my-spinner');\n * spinner.size = ctx.size === 'large' ? 48 : 24;\n * return spinner;\n * }\n * ```\n */\n loadingRenderer?: LoadingRenderer;\n\n // #endregion\n\n // #region Empty State\n\n /**\n * Custom renderer shown when the grid has no rows to display\n * (`loading === false` AND the rendered row count is `0`, after all plugin\n * processing such as filtering / grouping / server-side).\n *\n * - When **omitted**, a built-in message is rendered (\"No data to display\"\n * or \"No matching rows\" when source rows existed but were filtered out).\n * - When set to a function, the function receives an {@link EmptyContext}\n * and returns an `HTMLElement` or HTML string.\n * - When **explicitly `null`**, the empty overlay is suppressed entirely.\n *\n * The empty overlay is mutually exclusive with the loading overlay; if\n * `loading === true`, the loading overlay always wins.\n *\n * @example\n * ```typescript\n * // Show a backend error message via a closure over the consumer's state.\n * gridConfig.emptyRenderer = () =>\n * error\n * ? `Failed to load deals: ${error.message}`\n * : 'No deals to display';\n * ```\n *\n * @see {@link EmptyOverlay} to control where the overlay is mounted.\n * @since 2.12.0\n */\n emptyRenderer?: EmptyRenderer | null;\n\n /**\n * Where the empty-state overlay is mounted.\n *\n * - `'rows'` (default) — overlays the `.rows-container`. Headers stay\n * visible so users can clear filters or see the column schema.\n * - `'grid'` — overlays the `.tbw-grid-root`. Hides headers and any\n * shell/toolbar content too.\n *\n * @defaultValue `'rows'`\n * @since 2.12.0\n */\n emptyOverlay?: EmptyOverlay;\n\n // #endregion\n}\n// #endregion\n\n// #region Empty State Types\n\n/**\n * Where the empty-state overlay is mounted.\n *\n * @see {@link GridConfig.emptyOverlay}\n * @since 2.12.0\n */\nexport type EmptyOverlay = 'rows' | 'grid';\n\n/**\n * Context passed to a custom {@link EmptyRenderer}.\n *\n * @since 2.12.0\n */\nexport interface EmptyContext {\n /**\n * Number of rows in the source data before any plugin processing\n * (sort / filter / group / server-side). Equivalent to\n * `grid.sourceRows.length`.\n */\n sourceRowCount: number;\n /**\n * `true` when the source had rows but all of them were filtered out\n * (i.e. `sourceRowCount > 0` while the rendered row count is `0`).\n * The default renderer uses this flag to switch between\n * \"No data to display\" and \"No matching rows\".\n */\n filteredOut: boolean;\n}\n\n/**\n * Custom renderer for the empty state overlay.\n *\n * @param context - {@link EmptyContext} describing why the grid is empty.\n * @returns An `HTMLElement` or an HTML string.\n *\n * @example\n * ```typescript\n * const renderer: EmptyRenderer = (ctx) => {\n * const div = document.createElement('div');\n * div.textContent = ctx.filteredOut ? 'No matches' : 'No data';\n * return div;\n * };\n * ```\n *\n * @see {@link GridConfig.emptyRenderer}\n * @since 2.12.0\n */\nexport type EmptyRenderer = (context: EmptyContext) => HTMLElement | string;\n\n// #endregion\n\n// #region Animation\n\n/**\n * Sort state passed to custom sort handlers.\n * Represents the current sorting configuration for a column.\n *\n * @example\n * ```typescript\n * // In a custom sort handler\n * const sortHandler: SortHandler = (rows, sortState, columns) => {\n * const { field, direction } = sortState;\n * console.log(`Sorting by ${field} ${direction === 1 ? 'ASC' : 'DESC'}`);\n *\n * return [...rows].sort((a, b) => {\n * const aVal = a[field];\n * const bVal = b[field];\n * return (aVal < bVal ? -1 : aVal > bVal ? 1 : 0) * direction;\n * });\n * };\n * ```\n *\n * @see {@link SortHandler} for custom sort handler signature\n * @see {@link SortChangeDetail} for sort change events\n * @since 0.2.7\n */\nexport interface SortState {\n /** Field to sort by */\n field: string;\n /** Sort direction: 1 = ascending, -1 = descending */\n direction: 1 | -1;\n}\n\n/**\n * Custom sort handler function signature.\n *\n * :::caution\n * **Prefer {@link BaseColumnConfig.sortComparator} over `SortHandler`.**\n * `sortHandler` is bypassed by `MultiSortPlugin` and only sees the single\n * active sort field. For per-column custom sort logic that survives every\n * sort code path (core, multi-sort, tree, server-side), set `sortComparator`\n * on the relevant columns instead. For server-side sort, use\n * `ServerSideConfig.dataSource`.\n * :::\n *\n * Use `SortHandler` only when you need to replace the grid's sort engine\n * wholesale.\n *\n * @param rows - Current row array to sort\n * @param sortState - Sort field and direction\n * @param columns - Column configurations (for accessing sortComparator)\n * @returns Sorted array (sync) or Promise resolving to sorted array (async)\n *\n * @example\n * ```typescript\n * // Replace the built-in sort with a third-party stable sort library\n * const customSortHandler: SortHandler<Employee> = (rows, state) => {\n * return thirdPartyStableSort(rows, state.field, state.direction);\n * };\n *\n * grid.gridConfig = { sortHandler: customSortHandler };\n * ```\n *\n * @see {@link SortState} for the sort state object\n * @see {@link GridConfig.sortHandler} for configuring the handler (and its caveats)\n * @see {@link BaseColumnConfig.sortComparator} — recommended per-column comparator\n * @since 0.2.7\n */\nexport type SortHandler<TRow = any> = (\n rows: TRow[],\n sortState: SortState,\n columns: ColumnConfig<TRow>[],\n) => TRow[] | Promise<TRow[]>;\n\n// #region Loading\n\n/**\n * Loading indicator size variant.\n *\n * - `'large'`: 48x48px max - used for grid-level loading overlay (`grid.loading = true`)\n * - `'small'`: Follows row height - used for row/cell loading states\n *\n * @example\n * ```typescript\n * // Custom loading renderer adapting to size\n * const myLoader: LoadingRenderer = (ctx) => {\n * if (ctx.size === 'large') {\n * // Full overlay spinner\n * return '<div class=\"spinner-lg\"></div>';\n * }\n * // Inline row/cell spinner\n * return '<span class=\"spinner-sm\"></span>';\n * };\n * ```\n *\n * @see {@link LoadingRenderer} for custom loading renderer\n * @see {@link LoadingContext} for context passed to renderers\n * @since 1.7.0\n */\nexport type LoadingSize = 'large' | 'small';\n\n/**\n * Context passed to custom loading renderers.\n *\n * Provides information about the loading indicator being rendered,\n * allowing the renderer to adapt its appearance based on the size variant.\n *\n * @example\n * ```typescript\n * const myLoadingRenderer: LoadingRenderer = (ctx: LoadingContext) => {\n * if (ctx.size === 'large') {\n * // Full-size spinner for grid-level loading\n * return '<div class=\"large-spinner\"></div>';\n * } else {\n * // Compact spinner for row/cell loading\n * return '<div class=\"small-spinner\"></div>';\n * }\n * };\n * ```\n *\n * @see {@link LoadingRenderer} for the renderer function signature\n * @see {@link LoadingSize} for available size variants\n * @since 1.7.0\n */\nexport interface LoadingContext {\n /** The size variant being rendered: 'large' for grid-level, 'small' for row/cell */\n size: LoadingSize;\n}\n\n/**\n * Custom loading renderer function.\n * Returns an element or HTML string to display as the loading indicator.\n *\n * Used with the `loadingRenderer` property in {@link GridConfig} to replace\n * the default spinner with custom content.\n *\n * @param context - Context containing size information\n * @returns HTMLElement or HTML string\n *\n * @example\n * ```typescript\n * // Simple text loading indicator\n * const textLoader: LoadingRenderer = () => {\n * const el = document.createElement('div');\n * el.textContent = 'Loading...';\n * return el;\n * };\n *\n * // Custom spinner with size awareness\n * const customSpinner: LoadingRenderer = (ctx) => {\n * const spinner = document.createElement('my-spinner');\n * spinner.size = ctx.size === 'large' ? 48 : 24;\n * return spinner;\n * };\n *\n * // Material Design-style progress bar\n * const progressBar: LoadingRenderer = () => {\n * const container = document.createElement('div');\n * container.className = 'progress-bar-container';\n * container.innerHTML = '<div class=\"progress-bar\"></div>';\n * return container;\n * };\n *\n * grid.gridConfig = {\n * loadingRenderer: customSpinner,\n * };\n * ```\n *\n * @see {@link LoadingContext} for the context object passed to the renderer\n * @see {@link LoadingSize} for size variants ('large' | 'small')\n * @since 1.7.0\n */\nexport type LoadingRenderer = (context: LoadingContext) => HTMLElement | string;\n\n// #endregion\n\n// #region Data Change Event\n\n/**\n * Detail for the `data-change` event.\n *\n * Fired whenever the grid's row data changes — including new data assignment,\n * row insertion/removal, and in-place mutations via `updateRow()`.\n *\n * Use this to keep external UI in sync with the grid's current data state\n * (row counts, summaries, charts, etc.).\n *\n * @example\n * ```typescript\n * grid.on('data-change', ({ rowCount, sourceRowCount }) => {\n * console.log(`${rowCount} rows visible of ${sourceRowCount} total`);\n * });\n * ```\n *\n * @see {@link DataGridEventMap} for all event types\n * @category Events\n * @since 1.25.0\n */\nexport interface DataChangeDetail {\n /** Number of visible (processed) rows */\n rowCount: number;\n /** Total number of source rows (before filtering/grouping) */\n sourceRowCount: number;\n}\n\n/**\n * Detail payload for the `tbw-scroll` event.\n *\n * Dispatched on the grid host whenever the vertical viewport scrolls\n * (rAF-batched — at most one per frame). Use to trigger pagination,\n * defer heavy cell content rendering, dismiss overlays, or sync a\n * scroll-tracking UI outside the grid.\n *\n * For server-side pagination of large datasets, prefer\n * `ServerSidePlugin` which handles block fetching out of the box.\n *\n * @example\n * ```typescript\n * grid.addEventListener('tbw-scroll', (e) => {\n * const { scrollTop, scrollHeight, clientHeight } = e.detail;\n * if (scrollTop + clientHeight >= scrollHeight - 200) loadMore();\n * });\n * ```\n *\n * @see {@link DataGridEventMap} for all event types\n * @category Events\n * @since 2.2.0\n */\nexport interface TbwScrollDetail {\n /** Current vertical scroll offset in pixels (faux scrollbar). */\n scrollTop: number;\n /** Total scrollable height in pixels (faux scrollbar). */\n scrollHeight: number;\n /** Visible viewport height in pixels (faux scrollbar). */\n clientHeight: number;\n /**\n * Axis that triggered this dispatch. Currently always `'vertical'`;\n * `'horizontal'` is reserved for a future opt-in dispatch and is\n * declared up-front so consumer narrowing remains source-compatible.\n */\n direction: 'vertical' | 'horizontal';\n}\n\n// #endregion\n\n// #region Data Update Management\n\n/**\n * Indicates the origin of a data change.\n * Used to prevent infinite loops in cascade update handlers.\n *\n * - `'user'`: Direct user interaction via EditingPlugin (typing, selecting)\n * - `'cascade'`: Triggered by `updateRow()` in an event handler\n * - `'api'`: External programmatic update via `grid.updateRow()`\n *\n * @example\n * ```typescript\n * grid.on('cell-change', (detail) => {\n * const { source, field, newValue } = detail;\n *\n * // Only cascade updates for user edits\n * if (source === 'user' && field === 'price') {\n * // Update calculated field (marked as 'cascade')\n * grid.updateRow(detail.rowId, {\n * total: newValue * detail.row.quantity,\n * });\n * }\n *\n * // Ignore cascade updates to prevent infinite loops\n * if (source === 'cascade') return;\n * });\n * ```\n *\n * @see {@link CellChangeDetail} for the event detail containing source\n * @category Data Management\n * @since 1.0.0\n */\nexport type UpdateSource = 'user' | 'cascade' | 'api';\n\n/**\n * Detail for cell-change event (emitted by core after mutation).\n * This is an informational event that fires for ALL data mutations.\n *\n * Use this event for:\n * - Logging/auditing changes\n * - Cascading updates (updating other fields based on a change)\n * - Syncing changes to external state\n *\n * @example\n * ```typescript\n * grid.on('cell-change', ({ row, rowId, field, oldValue, newValue, source }) => {\n * console.log(`${field} changed from ${oldValue} to ${newValue}`);\n * console.log(`Change source: ${source}`);\n *\n * // Cascade: update total when price changes\n * if (source === 'user' && field === 'price') {\n * grid.updateRow(rowId, { total: newValue * row.quantity });\n * }\n * });\n * ```\n *\n * @see {@link UpdateSource} for understanding change origins\n * @see CellCommitDetail for the commit event (editing lifecycle)\n * @category Events\n * @since 1.0.0\n */\nexport interface CellChangeDetail<TRow = unknown> {\n /** The row object (after mutation) */\n row: TRow;\n /** Stable row identifier */\n rowId: string;\n /** Current index in rows array */\n rowIndex: number;\n /** Field that changed */\n field: string;\n /** Value before change */\n oldValue: unknown;\n /** Value after change */\n newValue: unknown;\n /** All changes passed to updateRow/updateRows (for context) */\n changes: Partial<TRow>;\n /** Origin of this change */\n source: UpdateSource;\n}\n\n/**\n * Batch update specification for updateRows().\n *\n * Used when you need to update multiple rows at once efficiently.\n * The grid will batch all updates and trigger a single re-render.\n *\n * @example\n * ```typescript\n * // Update multiple rows in a single batch\n * const updates: RowUpdate<Employee>[] = [\n * { id: 'emp-1', changes: { status: 'active', updatedAt: new Date() } },\n * { id: 'emp-2', changes: { status: 'inactive' } },\n * { id: 'emp-3', changes: { salary: 75000 } },\n * ];\n *\n * grid.updateRows(updates);\n * ```\n *\n * @see {@link CellChangeDetail} for individual change events\n * @see {@link GridConfig.getRowId} for row identification\n * @category Data Management\n * @since 1.0.0\n */\nexport interface RowUpdate<TRow = unknown> {\n /** Row identifier (from getRowId) */\n id: string;\n /** Fields to update */\n changes: Partial<TRow>;\n}\n\n/**\n * A batch of row mutations to apply atomically in a single render cycle.\n *\n * All adds, updates, and removes are processed together with one re-render,\n * making this far more efficient than calling `insertRow`, `updateRow`, and\n * `removeRow` individually — especially for high-frequency streaming data.\n *\n * Row identification for `update` and `remove` uses the grid's configured\n * {@link GridConfig.getRowId | getRowId} function.\n *\n * @example\n * ```typescript\n * // Apply a mixed transaction from a WebSocket message\n * const result = await grid.applyTransaction({\n * add: [{ id: 'new-1', name: 'Alice', status: 'Active' }],\n * update: [{ id: 'emp-5', changes: { status: 'Inactive' } }],\n * remove: [{ id: 'emp-3' }],\n * });\n *\n * console.log(`Added: ${result.added.length}, Updated: ${result.updated.length}, Removed: ${result.removed.length}`);\n * ```\n *\n * @see {@link TransactionResult} for the result structure\n * @category Data Management\n * @since 1.27.0\n */\nexport interface RowTransaction<TRow = unknown> {\n /** Rows to insert. Appended at the end of the current view. */\n add?: TRow[];\n /** Rows to update in-place by ID. */\n update?: RowUpdate<TRow>[];\n /** Rows to remove by ID. */\n remove?: Array<{ id: string }>;\n}\n\n/**\n * Result of a {@link RowTransaction} applied via `applyTransaction`.\n *\n * Contains the actual row objects that were affected, useful for\n * post-processing or logging.\n *\n * @see {@link RowTransaction} for the input structure\n * @category Data Management\n * @since 1.27.0\n */\nexport interface TransactionResult<TRow = unknown> {\n /** Rows that were successfully added. */\n added: TRow[];\n /** Rows that were successfully updated (references to the mutated row objects). */\n updated: TRow[];\n /** Rows that were successfully removed. */\n removed: TRow[];\n}\n\n// #endregion\n\n/**\n * Animation behavior mode.\n * - `true` or `'on'`: Animations always enabled\n * - `false` or `'off'`: Animations always disabled\n * - `'reduced-motion'`: Respects `prefers-reduced-motion` media query (default)\n *\n * @example\n * ```typescript\n * // Force animations on (ignore system preference)\n * grid.gridConfig = { animation: { mode: 'on' } };\n *\n * // Disable all animations\n * grid.gridConfig = { animation: { mode: false } };\n *\n * // Respect user's accessibility settings (default)\n * grid.gridConfig = { animation: { mode: 'reduced-motion' } };\n * ```\n *\n * @see {@link AnimationConfig} for full animation configuration\n * @since 0.2.7\n */\nexport type AnimationMode = boolean | 'on' | 'off' | 'reduced-motion';\n\n/**\n * Animation style for visual transitions.\n * - `'slide'`: Slide/transform animation (e.g., expand down, slide left/right)\n * - `'fade'`: Opacity fade animation\n * - `'flip'`: FLIP technique for position changes (First, Last, Invert, Play)\n * - `false`: No animation for this specific feature\n *\n * @example\n * ```typescript\n * // Plugin-specific animation styles\n * new TreePlugin({\n * expandAnimation: 'slide', // Slide children down when expanding\n * });\n *\n * new ReorderPlugin({\n * animation: 'flip', // FLIP animation for column reordering\n * });\n * ```\n *\n * @see {@link AnimationConfig} for grid-wide animation settings\n * @see {@link ExpandCollapseAnimation} for expand/collapse-specific styles\n * @since 0.2.7\n */\nexport type AnimationStyle = 'slide' | 'fade' | 'flip' | false;\n\n/**\n * Animation style for expand/collapse operations.\n * Subset of AnimationStyle - excludes 'flip' which is for position changes.\n * - `'slide'`: Slide down/up animation for expanding/collapsing content\n * - `'fade'`: Fade in/out animation\n * - `false`: No animation\n *\n * @example\n * ```typescript\n * // Tree rows slide down when expanding\n * new TreePlugin({ expandAnimation: 'slide' });\n *\n * // Row groups fade in/out\n * new GroupingRowsPlugin({ expandAnimation: 'fade' });\n *\n * // Master-detail panels with no animation\n * new MasterDetailPlugin({ expandAnimation: false });\n * ```\n *\n * @see {@link AnimationStyle} for all animation styles\n * @see {@link AnimationConfig} for grid-wide settings\n * @since 0.2.9\n */\nexport type ExpandCollapseAnimation = 'slide' | 'fade' | false;\n\n/**\n * Type of row animation.\n * - `'change'`: Flash highlight when row data changes (e.g., after cell edit)\n * - `'insert'`: Slide-in animation for newly added rows\n * - `'remove'`: Fade-out animation for rows being removed\n *\n * @example\n * ```typescript\n * // Internal usage - row animation is triggered automatically:\n * // - 'change' after cell-commit event\n * // - 'insert' when rows are added to the grid\n * // - 'remove' when rows are deleted\n *\n * // The animation respects AnimationConfig.mode\n * grid.gridConfig = {\n * animation: { mode: 'on', duration: 300 },\n * };\n * ```\n *\n * @see {@link AnimationConfig} for animation configuration\n * @since 1.3.0\n */\nexport type RowAnimationType = 'change' | 'insert' | 'remove';\n\n/**\n * Grid-wide animation configuration.\n * Controls global animation behavior - individual plugins define their own animation styles.\n * Duration and easing values set corresponding CSS variables on the grid element.\n *\n * @example\n * ```typescript\n * // Enable animations regardless of system preferences\n * grid.gridConfig = {\n * animation: {\n * mode: 'on',\n * duration: 300,\n * easing: 'cubic-bezier(0.4, 0, 0.2, 1)',\n * },\n * };\n *\n * // Disable all animations\n * grid.gridConfig = {\n * animation: { mode: 'off' },\n * };\n *\n * // Respect user's reduced-motion preference (default)\n * grid.gridConfig = {\n * animation: { mode: 'reduced-motion' },\n * };\n * ```\n *\n * @see {@link AnimationMode} for mode options\n * @since 0.2.7\n */\nexport interface AnimationConfig {\n /**\n * Global animation mode.\n * @default 'reduced-motion'\n */\n mode?: AnimationMode;\n\n /**\n * Default animation duration in milliseconds.\n * Sets `--tbw-animation-duration` CSS variable.\n * @default 200\n */\n duration?: number;\n\n /**\n * Default easing function.\n * Sets `--tbw-animation-easing` CSS variable.\n * @default 'ease-out'\n */\n easing?: string;\n}\n\n/** Default animation configuration * @since 0.2.7\n */\nexport const DEFAULT_ANIMATION_CONFIG: Required<Omit<AnimationConfig, 'sort'>> = {\n mode: 'reduced-motion',\n duration: 200,\n easing: 'ease-out',\n};\n\n// #endregion\n\n// #region Grid Icons\n\n/** Icon value - can be a string (text/HTML) or HTMLElement * @since 0.1.1\n */\nexport type IconValue = string | HTMLElement;\n\n/**\n * Grid-wide icon configuration.\n * All icons are optional - sensible defaults are used when not specified.\n *\n * The grid uses a **CSS-first hybrid approach**: icons render via `--tbw-icon-*` CSS\n * custom properties by default. Setting `gridConfig.icons` provides JS overrides that\n * take precedence over CSS (the JS icon injects DOM content, suppressing the CSS\n * `::before` pseudo-element via the `:empty` selector).\n *\n * **Use CSS** for static theming (text, emoji, SVG masks via `--tbw-icon-*-mask`).\n * **Use JS** for dynamic icons, icon libraries, or `HTMLElement` instances.\n *\n * Icons can be text (including emoji), HTML strings (for SVG), or HTMLElement instances.\n *\n * @example\n * ```typescript\n * grid.gridConfig = {\n * icons: {\n * // Emoji icons\n * expand: '➕',\n * collapse: '➖',\n *\n * // Custom SVG icon\n * sortAsc: '<svg viewBox=\"0 0 16 16\"><path d=\"M8 4l4 8H4z\"/></svg>',\n *\n * // Font icon class (wrap in span)\n * filter: '<span class=\"icon icon-filter\"></span>',\n * },\n * };\n * ```\n *\n * @see {@link IconValue} for allowed icon formats\n * @since 0.1.1\n */\nexport interface GridIcons {\n /** Expand icon for collapsed items (trees, groups, details). Default: '▶' */\n expand?: IconValue;\n /** Collapse icon for expanded items (trees, groups, details). Default: '▼' */\n collapse?: IconValue;\n /** Sort ascending indicator. Default: '▲' */\n sortAsc?: IconValue;\n /** Sort descending indicator. Default: '▼' */\n sortDesc?: IconValue;\n /** Sort neutral/unsorted indicator. Default: '⇅' */\n sortNone?: IconValue;\n /** Submenu arrow for context menus. Default: '▶' */\n submenuArrow?: IconValue;\n /** Drag handle icon for reordering. Default: '⋮⋮' */\n dragHandle?: IconValue;\n /** Tool panel toggle icon in toolbar. Default: '☰' */\n toolPanel?: IconValue;\n /** Filter icon in column headers. Default: SVG funnel icon */\n filter?: IconValue;\n /** Filter icon when filter is active. Default: same as filter with accent color */\n filterActive?: IconValue;\n /** Print icon for print button. Default: '🖨️' */\n print?: IconValue;\n}\n\n/** Default icons used when not overridden. Most entries are short text/emoji;\n * `filter` and `filterActive` are empty strings because the actual rendering\n * is driven by the `--tbw-icon-filter[-active]-mask` CSS custom properties\n * (see `core/styles/variables.css`). Userland that wants an HTML/SVG fallback\n * via `gridConfig.icons.filter = '<svg…>'` is unaffected — that path overrides this default. * @since 0.1.1\n */\nexport const DEFAULT_GRID_ICONS: Required<GridIcons> = {\n expand: '▶',\n collapse: '▼',\n sortAsc: '▲',\n sortDesc: '▼',\n sortNone: '⇅',\n submenuArrow: '▶',\n dragHandle: '⋮⋮',\n toolPanel: '☰',\n filter: '',\n filterActive: '',\n print: '🖨️',\n};\n// #endregion\n\n// #region Column State (Persistence)\n\n/**\n * State for a single column. Captures user-driven changes at runtime.\n * Plugins can extend this interface via module augmentation to add their own state.\n *\n * Used with `grid.getColumnState()` and `grid.columnState` for persisting\n * user customizations (column widths, order, visibility, sort).\n *\n * @example\n * ```typescript\n * // Save column state to localStorage\n * const state = grid.getColumnState();\n * localStorage.setItem('gridState', JSON.stringify(state));\n *\n * // Restore on page load\n * const saved = localStorage.getItem('gridState');\n * if (saved) grid.applyColumnState(JSON.parse(saved));\n *\n * // Example column state structure\n * const state: GridColumnState = {\n * columns: [\n * { field: 'name', order: 0, width: 200, hidden: false },\n * { field: 'email', order: 1, width: 300, hidden: false },\n * { field: 'phone', order: 2, hidden: true }, // Hidden column\n * ],\n * sort: { field: 'name', direction: 1 },\n * };\n * ```\n *\n * @example\n * ```typescript\n * // Plugin augmentation example (in filtering plugin)\n * declare module '@toolbox-web/grid' {\n * interface ColumnState {\n * filter?: FilterValue;\n * }\n * }\n * ```\n *\n * @see {@link GridColumnState} for the full state object\n * @since 0.1.1\n */\nexport interface ColumnState {\n /** Column field identifier */\n field: string;\n /** Position index after reordering (0-based) */\n order: number;\n /** Width in pixels (undefined = use default) */\n width?: number;\n /** Visibility state */\n visible: boolean;\n /** Sort state (undefined = not sorted). */\n sort?: ColumnSortState;\n}\n\n/**\n * Sort state for a column.\n * Used within {@link ColumnState} to track sort direction and priority.\n *\n * @see {@link ColumnState} for column state persistence\n * @see {@link SortChangeDetail} for sort change events\n * @since 0.1.1\n */\nexport interface ColumnSortState {\n /** Sort direction */\n direction: 'asc' | 'desc';\n /** Priority for multi-sort (0 = primary, 1 = secondary, etc.) */\n priority: number;\n}\n\n/**\n * Complete grid column state for persistence.\n * Contains state for all columns, including plugin-contributed properties.\n *\n * @example\n * ```typescript\n * // Save state\n * const state = grid.getColumnState();\n * localStorage.setItem('grid-state', JSON.stringify(state));\n *\n * // Restore state\n * grid.applyColumnState(JSON.parse(localStorage.getItem('grid-state')));\n * ```\n *\n * @see {@link ColumnState} for individual column state\n * @see {@link PublicGrid.getColumnState} for retrieving state\n * @since 0.1.1\n */\nexport interface GridColumnState {\n /** Array of column states. */\n columns: ColumnState[];\n}\n// #endregion\n\n// #region Public Event Detail Interfaces\n/**\n * Detail for the {@link DataGridEventMap.render | `render`} event.\n *\n * Fired once at the end of every render cycle (the single RAF flush in the\n * render scheduler), after all plugin `afterRender` hooks have run and after\n * `grid.ready()` has resolved.\n *\n * Use this when you need to act on the rendered DOM (e.g. focus the first\n * input of a freshly added row when `editing.mode === 'grid'`) without\n * resorting to `setTimeout` or double-`requestAnimationFrame` hacks.\n *\n * @example\n * ```typescript\n * // Focus the first cell's input after adding a row in full-grid edit mode\n * function addEmployee() {\n * grid.addRow({ id: crypto.randomUUID(), name: '', email: '' });\n * grid.addEventListener(\n * 'render',\n * () => {\n * const input = grid.querySelector<HTMLInputElement>(\n * '[data-row=\"0\"][data-col=\"0\"] input',\n * );\n * input?.focus();\n * },\n * { once: true },\n * );\n * }\n * ```\n *\n * @category Events\n * @since 2.15.0\n */\nexport interface RenderDetail {\n /**\n * The highest render phase that executed this cycle (see {@link RenderPhase}).\n * Use this to skip cheap scroll-only renders (`phase < RenderPhase.ROWS`)\n * if you only care about row/column model changes.\n */\n phase: RenderPhase;\n /** `true` only for the very first render after the grid was connected. */\n initial: boolean;\n /** Number of rows in the effective row model after plugin `processRows` hooks ran. */\n rowCount: number;\n /**\n * The visible virtual window — `start` inclusive, `end` exclusive — or `null`\n * when virtualization is disabled (small datasets below the bypass threshold).\n *\n * When virtualization is enabled but no rows are visible (e.g. empty dataset)\n * this is `{ start: 0, end: 0 }`, NOT `null` — that lets consumers distinguish\n * \"virtualization disabled\" from \"enabled but currently empty\".\n */\n visibleRange: { start: number; end: number } | null;\n}\n\n/**\n * Detail for a cell click event.\n * Provides full context about the clicked cell including row data.\n *\n * @example\n * ```typescript\n * grid.on('cell-click', ({ row, field, value, rowIndex, colIndex }) => {\n * console.log(`Clicked ${field} = ${value} in row ${rowIndex}`);\n *\n * // Access the full row data\n * if (row.status === 'pending') {\n * showApprovalDialog(row);\n * }\n * });\n * ```\n *\n * @category Events\n * @since 1.0.0\n */\nexport interface CellClickDetail<TRow = unknown> {\n /** Zero-based row index of the clicked cell. */\n rowIndex: number;\n /** Zero-based column index of the clicked cell. */\n colIndex: number;\n /** Column configuration object for the clicked cell. */\n column: ColumnConfig<TRow>;\n /** Field name of the clicked column. */\n field: string;\n /** Cell value at the clicked position. */\n value: unknown;\n /** Full row data object. */\n row: TRow;\n /** The clicked cell element. */\n cellEl: HTMLElement;\n /** The original mouse event. */\n originalEvent: MouseEvent;\n}\n\n/**\n * Detail for a row click event.\n * Provides context about the clicked row.\n *\n * @example\n * ```typescript\n * grid.on('row-click', ({ row, rowIndex, rowEl }) => {\n * console.log(`Clicked row ${rowIndex}: ${row.name}`);\n *\n * // Highlight the row\n * rowEl.classList.add('selected');\n *\n * // Open detail panel\n * showDetailPanel(row);\n * });\n * ```\n *\n * @category Events\n * @since 1.0.0\n */\nexport interface RowClickDetail<TRow = unknown> {\n /** Zero-based row index of the clicked row. */\n rowIndex: number;\n /** Full row data object. */\n row: TRow;\n /** The clicked row element. */\n rowEl: HTMLElement;\n /** The original mouse event. */\n originalEvent: MouseEvent;\n}\n\n/**\n * Detail for a sort change (direction 0 indicates cleared sort).\n *\n * @example\n * ```typescript\n * grid.on('sort-change', ({ field, direction }) => {\n * if (direction === 0) {\n * console.log(`Sort cleared on ${field}`);\n * } else {\n * const dir = direction === 1 ? 'ascending' : 'descending';\n * console.log(`Sorted by ${field} ${dir}`);\n * }\n *\n * // Fetch sorted data from server\n * fetchData({ sortBy: field, sortDir: direction });\n * });\n * ```\n *\n * @see {@link SortState} for the sort state object\n * @see {@link SortHandler} for custom sort handlers\n * @category Events\n * @since 0.1.1\n */\nexport interface SortChangeDetail {\n /** Sorted field key. */\n field: string;\n /** Direction: 1 ascending, -1 descending, 0 cleared. */\n direction: 1 | -1 | 0;\n}\n\n/**\n * Column resize event detail containing final pixel width.\n *\n * @example\n * ```typescript\n * grid.on('column-resize', ({ field, width }) => {\n * console.log(`Column ${field} resized to ${width}px`);\n *\n * // Persist to user preferences\n * saveColumnWidth(field, width);\n * });\n * ```\n *\n * @see {@link ColumnState} for persisting column state\n * @see {@link ResizeController} for resize implementation\n * @category Events\n * @since 0.1.1\n */\nexport interface ColumnResizeDetail {\n /** Resized column field key. */\n field: string;\n /** New width in pixels. */\n width: number;\n}\n\n/**\n * Column resize-reset event detail.\n *\n * Fired when a user-resized column is restored to its original configured width\n * (e.g., via the column header context menu \"Reset width\" action). The `width`\n * field reflects the column's `__originalWidth` and may be `undefined` if the\n * column was originally auto-sized.\n *\n * @example\n * ```typescript\n * grid.on('column-resize-reset', ({ field, width }) => {\n * if (width === undefined) {\n * console.log(`Column ${field} restored to auto-size`);\n * } else {\n * console.log(`Column ${field} restored to ${width}px`);\n * }\n * });\n * ```\n *\n * @see {@link ColumnResizeDetail} for the resize-in-progress event\n * @category Events\n * @since 2.6.0\n */\nexport interface ColumnResizeResetDetail {\n /** Reset column field key. */\n field: string;\n /** Original configured width in pixels, or `undefined` if auto-sized. */\n width: number | undefined;\n}\n\n/**\n * Trigger type for cell activation.\n * - `'keyboard'`: Enter key pressed on focused cell\n * - `'pointer'`: Mouse/touch/pen click on cell\n *\n * @see {@link CellActivateDetail} for the activation event detail\n * @category Events\n * @since 1.0.0\n */\nexport type CellActivateTrigger = 'keyboard' | 'pointer';\n\n/**\n * Fired when a cell is activated by user interaction (Enter key or click).\n * Unified event for both keyboard and pointer activation.\n *\n * @example\n * ```typescript\n * grid.on('cell-activate', ({ row, field, value, trigger, cellEl }, event) => {\n * if (trigger === 'keyboard') {\n * console.log('Activated via Enter key');\n * } else {\n * console.log('Activated via click/tap');\n * }\n *\n * // Start custom editing for specific columns\n * if (field === 'notes') {\n * event.preventDefault(); // Prevent default editing\n * openNotesEditor(row, cellEl);\n * }\n * });\n * ```\n *\n * @see {@link CellClickDetail} for click-only events\n * @see {@link CellActivateTrigger} for trigger types\n * @category Events\n * @since 1.0.0\n */\nexport interface CellActivateDetail<TRow = unknown> {\n /** Zero-based row index of the activated cell. */\n rowIndex: number;\n /** Zero-based column index of the activated cell. */\n colIndex: number;\n /** Field name of the activated column. */\n field: string;\n /** Cell value at the activated position. */\n value: unknown;\n /** Full row data object. */\n row: TRow;\n /** The activated cell element. */\n cellEl: HTMLElement;\n /** What triggered the activation. */\n trigger: CellActivateTrigger;\n /** The original event (KeyboardEvent for keyboard, MouseEvent/PointerEvent for pointer). */\n originalEvent: KeyboardEvent | MouseEvent | PointerEvent;\n}\n\n/**\n * Event detail for mounting external view renderers.\n *\n * Emitted when a cell uses an external component spec (React, Angular, Vue)\n * and needs the framework adapter to mount the component.\n *\n * @example\n * ```typescript\n * // Framework adapter listens for this event\n * grid.on('mount-external-view', ({ placeholder, spec, context }) => {\n * // Mount framework component into placeholder\n * mountComponent(spec.component, placeholder, context);\n * });\n * ```\n *\n * @see {@link ColumnConfig.externalView} for external view spec\n * @see {@link FrameworkAdapter} for adapter interface\n * @category Framework Adapters\n * @since 0.1.1\n */\nexport interface ExternalMountViewDetail<TRow = unknown> {\n placeholder: HTMLElement;\n spec: unknown;\n context: { row: TRow; value: unknown; field: string; column: unknown };\n}\n\n/**\n * Event detail for mounting external editor renderers.\n *\n * Emitted when a cell uses an external editor component spec and needs\n * the framework adapter to mount the editor with commit/cancel bindings.\n *\n * @example\n * ```typescript\n * // Framework adapter listens for this event\n * grid.on('mount-external-editor', ({ placeholder, spec, context }) => {\n * // Mount framework editor with commit/cancel wired\n * mountEditor(spec.component, placeholder, {\n * value: context.value,\n * onCommit: context.commit,\n * onCancel: context.cancel,\n * });\n * });\n * ```\n *\n * @see {@link ColumnEditorSpec} for external editor spec\n * @see {@link FrameworkAdapter} for adapter interface\n * @category Framework Adapters\n * @since 0.1.1\n */\nexport interface ExternalMountEditorDetail<TRow = unknown> {\n placeholder: HTMLElement;\n spec: unknown;\n context: {\n row: TRow;\n value: unknown;\n field: string;\n column: unknown;\n commit: (v: unknown) => void;\n cancel: () => void;\n };\n}\n\n/**\n * Maps event names to their detail payload types.\n *\n * Used by {@link DataGridElement.on | grid.on()} and `addEventListener()` for\n * fully typed event handling. Plugins extend this map via module augmentation.\n *\n * @example\n * ```typescript\n * // Recommended: grid.on() auto-unwraps the detail\n * const off = grid.on('cell-click', ({ field, value, row }) => {\n * console.log(`Clicked ${field} = ${value}`);\n * });\n * off(); // unsubscribe\n *\n * // addEventListener works too (useful for { once, signal, capture })\n * grid.addEventListener('cell-click', (e) => {\n * console.log(e.detail.field);\n * }, { once: true });\n * ```\n *\n * @see {@link DataGridElement.on} for the recommended subscription API\n * @see {@link DataGridCustomEvent} for typed CustomEvent wrapper\n * @category Events\n * @since 0.1.1\n */\nexport interface DataGridEventMap<TRow = unknown> {\n /**\n * Fired when a cell is clicked.\n * Provides full context: row data, column config, cell element, and the original mouse event.\n *\n * @example\n * ```typescript\n * grid.on('cell-click', ({ row, field, value, cellEl }) => {\n * console.log(`Clicked ${field} = ${value}`);\n *\n * // Open a detail dialog for a specific column\n * if (field === 'avatar') {\n * showImagePreview(row.avatarUrl, cellEl);\n * }\n * });\n * ```\n *\n * @see {@link CellClickDetail}\n * @group Core Events\n */\n 'cell-click': CellClickDetail<TRow>;\n\n /**\n * Fired when a row is clicked (anywhere on the row).\n * Use for row-level actions like opening a detail panel or navigating.\n *\n * @example\n * ```typescript\n * grid.on('row-click', ({ row, rowIndex }) => {\n * console.log(`Row ${rowIndex}: ${row.name}`);\n *\n * // Navigate to detail page\n * router.navigate(`/employees/${row.id}`);\n * });\n * ```\n *\n * @see {@link RowClickDetail}\n * @group Core Events\n */\n 'row-click': RowClickDetail<TRow>;\n\n /**\n * Fired when a cell is activated by Enter key or pointer click.\n * Unified event for both keyboard and pointer activation.\n *\n * Call `event.preventDefault()` to suppress default behavior (e.g., inline editing).\n *\n * @example\n * ```typescript\n * grid.on('cell-activate', ({ row, field, trigger, cellEl }, event) => {\n * // Custom editing for a specific column\n * if (field === 'notes') {\n * event.preventDefault();\n * openRichTextEditor(row, cellEl);\n * }\n *\n * console.log(`Activated via ${trigger}`); // 'keyboard' | 'pointer'\n * });\n * ```\n *\n * @see {@link CellActivateDetail}\n * @see {@link CellActivateTrigger}\n * @group Core Events\n */\n 'cell-activate': CellActivateDetail<TRow>;\n\n /**\n * Fired after any data mutation — user edits, cascade updates, or API calls.\n * This is an informational event for logging, auditing, or cascading updates\n * to related fields. Check `source` to distinguish edit origins.\n *\n * @example\n * ```typescript\n * grid.on('cell-change', ({ row, rowId, field, oldValue, newValue, source }) => {\n * console.log(`${field}: ${oldValue} → ${newValue} (${source})`);\n *\n * // Cascade: recalculate total when quantity changes\n * if (source === 'user' && field === 'quantity') {\n * grid.updateRow(rowId, { total: newValue * row.price });\n * }\n * });\n * ```\n *\n * @see {@link CellChangeDetail}\n * @see {@link UpdateSource}\n * @group Core Events\n */\n 'cell-change': CellChangeDetail<TRow>;\n\n /**\n * Fired whenever the grid's row data changes — new data assignment,\n * row insertion/removal, or in-place mutations via `updateRow()`.\n * Use to keep external UI (row counts, summaries, charts) in sync.\n *\n * @example\n * ```typescript\n * grid.on('data-change', ({ rowCount, sourceRowCount }) => {\n * statusBar.textContent = `${rowCount} of ${sourceRowCount} rows`;\n * });\n * ```\n *\n * @see {@link DataChangeDetail}\n * @group Core Events\n */\n 'data-change': DataChangeDetail;\n\n /**\n * Fired when the grid's viewport is scrolled vertically (rAF-batched).\n * Use to trigger pagination, defer heavy cell content, dismiss overlays,\n * or sync external UI to the grid's scroll position.\n *\n * For server-side pagination of large datasets, prefer the\n * `ServerSidePlugin` — it handles block fetching out of the box.\n *\n * @example\n * ```typescript\n * // Infinite scroll\n * grid.on('tbw-scroll', ({ scrollTop, scrollHeight, clientHeight }) => {\n * if (scrollTop + clientHeight >= scrollHeight - 200) loadMore();\n * });\n *\n * // Defer heavy cell content (Angular @defer style)\n * grid.on('tbw-scroll', ({ scrollTop }) => {\n * updateVisibleHeavyCells(scrollTop);\n * });\n *\n * // Dismiss tooltip/popover on scroll\n * grid.on('tbw-scroll', () => closeOpenOverlays());\n * ```\n *\n * @see {@link TbwScrollDetail}\n * @group Core Events\n */\n 'tbw-scroll': TbwScrollDetail;\n\n /**\n * Emitted when a cell with an external view renderer (React, Angular, Vue component)\n * needs to be mounted. Framework adapters listen for this event internally.\n *\n * @example\n * ```typescript\n * // Custom framework adapter\n * grid.on('mount-external-view', ({ placeholder, spec, context }) => {\n * myFramework.render(spec.component, placeholder, {\n * row: context.row,\n * value: context.value,\n * });\n * });\n * ```\n *\n * @see {@link ExternalMountViewDetail}\n * @see {@link FrameworkAdapter}\n * @group Framework Adapter Events\n */\n 'mount-external-view': ExternalMountViewDetail<TRow>;\n\n /**\n * Emitted when a cell with an external editor component (React, Angular, Vue)\n * needs to be mounted with commit/cancel bindings. Framework adapters listen\n * for this event internally.\n *\n * @example\n * ```typescript\n * // Custom framework adapter\n * grid.on('mount-external-editor', ({ placeholder, spec, context }) => {\n * myFramework.render(spec.component, placeholder, {\n * value: context.value,\n * onSave: context.commit,\n * onCancel: context.cancel,\n * });\n * });\n * ```\n *\n * @see {@link ExternalMountEditorDetail}\n * @see {@link FrameworkAdapter}\n * @group Framework Adapter Events\n */\n 'mount-external-editor': ExternalMountEditorDetail<TRow>;\n\n /**\n * Fired when the sort state changes — column header click, programmatic sort,\n * or sort cleared. `direction: 0` indicates the sort was removed.\n *\n * @example\n * ```typescript\n * grid.on('sort-change', ({ field, direction }) => {\n * if (direction === 0) {\n * console.log('Sort cleared');\n * } else {\n * console.log(`Sorted by ${field} ${direction === 1 ? 'ASC' : 'DESC'}`);\n * }\n *\n * // Server-side sorting\n * fetchData({ sortBy: field, sortDir: direction });\n * });\n * ```\n *\n * @see {@link SortChangeDetail}\n * @see {@link SortHandler}\n * @group Core Events\n */\n 'sort-change': SortChangeDetail;\n\n /**\n * Fired when a column is resized by the user dragging the resize handle.\n * Use to persist column widths to user preferences or localStorage.\n *\n * @example\n * ```typescript\n * grid.on('column-resize', ({ field, width }) => {\n * console.log(`Column \"${field}\" resized to ${width}px`);\n *\n * // Persist to localStorage\n * const widths = JSON.parse(localStorage.getItem('col-widths') ?? '{}');\n * widths[field] = width;\n * localStorage.setItem('col-widths', JSON.stringify(widths));\n * });\n * ```\n *\n * @see {@link ColumnResizeDetail}\n * @group Core Events\n */\n 'column-resize': ColumnResizeDetail;\n\n /**\n * Fired when a user-resized column is reset to its original configured width.\n * Triggered by the column header context menu \"Reset width\" action.\n *\n * @example\n * ```typescript\n * grid.on('column-resize-reset', ({ field, width }) => {\n * const widths = JSON.parse(localStorage.getItem('col-widths') ?? '{}');\n * delete widths[field];\n * localStorage.setItem('col-widths', JSON.stringify(widths));\n * });\n * ```\n *\n * @see {@link ColumnResizeResetDetail}\n * @group Core Events\n */\n 'column-resize-reset': ColumnResizeResetDetail;\n\n /**\n * Fired when column state changes — reordering, resizing, visibility toggle,\n * or sort changes. Use with `getColumnState()` / `columnState` setter for\n * full state persistence.\n *\n * @example\n * ```typescript\n * grid.on('column-state-change', (state) => {\n * localStorage.setItem('grid-state', JSON.stringify(state));\n * console.log(`${state.columns.length} columns in state`);\n * });\n *\n * // Restore on load\n * const saved = localStorage.getItem('grid-state');\n * if (saved) grid.applyColumnState(JSON.parse(saved));\n * ```\n *\n * @see {@link GridColumnState}\n * @see {@link ColumnState}\n * @group Core Events\n */\n 'column-state-change': GridColumnState;\n\n /**\n * Fired once at the end of every render cycle, after all plugin\n * `afterRender` hooks have run and `ready()` has resolved.\n *\n * This is the canonical post-render hook for consumers. Use it instead of\n * `setTimeout` / double-`requestAnimationFrame` to act on the rendered DOM\n * after a programmatic mutation.\n *\n * Note: `ready()` only resolves once (after the first render). The `render`\n * event fires on every flush — including scroll-driven virtual-window\n * updates — so prefer `{ once: true }` (or check `detail.phase`) when you\n * want to act on a specific mutation.\n *\n * @example\n * ```typescript\n * // Focus the first cell's input after addRow in full-grid edit mode\n * grid.addRow({ id: crypto.randomUUID(), name: '', email: '' });\n * grid.addEventListener(\n * 'render',\n * () => {\n * const input = grid.querySelector<HTMLInputElement>(\n * '[data-row=\"0\"][data-col=\"0\"] input',\n * );\n * input?.focus();\n * },\n * { once: true },\n * );\n * ```\n *\n * @see {@link RenderDetail}\n * @group Core Events\n * @since 2.15.0\n */\n render: RenderDetail;\n\n // Note: 'cell-commit', 'row-commit', 'changed-rows-reset' are added via\n // module augmentation by EditingPlugin when imported\n}\n\n/**\n * Extracts the event detail type for a given event name.\n *\n * Utility type for getting the detail payload type of a specific event.\n *\n * @example\n * ```typescript\n * // Extract detail type for specific event\n * type ClickDetail = DataGridEventDetail<'cell-click', Employee>;\n * // Equivalent to: CellClickDetail<Employee>\n *\n * // Use in generic handler\n * function logDetail<K extends keyof DataGridEventMap>(\n * eventName: K,\n * detail: DataGridEventDetail<K>,\n * ): void {\n * console.log(`${eventName}:`, detail);\n * }\n * ```\n *\n * @see {@link DataGridEventMap} for all event types\n * @category Events\n */\nexport type DataGridEventDetail<K extends keyof DataGridEventMap<unknown>, TRow = unknown> = DataGridEventMap<TRow>[K];\n\n/**\n * Custom event type for DataGrid events with typed detail payload.\n *\n * Primarily useful when you need to declare handler parameters with\n * `addEventListener`. For most use cases, prefer {@link DataGridElement.on | grid.on()}\n * which handles typing automatically.\n *\n * @example\n * ```typescript\n * // Typed handler for addEventListener\n * function onCellClick(e: DataGridCustomEvent<'cell-click', Employee>): void {\n * const { row, field, value } = e.detail;\n * console.log(`Clicked ${field} = ${value} on ${row.name}`);\n * }\n * grid.addEventListener('cell-click', onCellClick);\n *\n * // With grid.on() you don't need this type — it's inferred:\n * grid.on('cell-click', ({ row, field, value }) => {\n * console.log(`Clicked ${field} = ${value} on ${row.name}`);\n * });\n * ```\n *\n * @see {@link DataGridElement.on} for the recommended subscription API\n * @see {@link DataGridEventMap} for all event types\n * @see `DataGridEventDetail` for extracting detail type only\n * @category Events\n * @since 0.1.1\n */\nexport type DataGridCustomEvent<K extends keyof DataGridEventMap<unknown>, TRow = unknown> = CustomEvent<\n DataGridEventMap<TRow>[K]\n>;\n\n/**\n * Template evaluation context for dynamic templates.\n *\n * @category Plugin Development\n * @since 0.1.1\n */\nexport interface EvalContext {\n value: unknown;\n row: Record<string, unknown> | null;\n typeDefault?: Record<string, unknown> | null;\n}\n// #endregion\n","/**\n * Base Grid Plugin Class\n *\n * All plugins extend this abstract class.\n * Plugins are instantiated per-grid and manage their own state.\n */\n\n// Injected by Vite at build time from package.json (same as grid.ts)\ndeclare const __GRID_VERSION__: string;\n\nimport type { HeaderContentDefinition, ToolPanelDefinition } from '../../plugins/shell/types';\nimport {\n type DiagnosticCode,\n PLUGIN_ALIAS_CONFIG_CONFLICT,\n formatDiagnostic,\n gridPrefix,\n} from '../internal/diagnostics';\nimport { sanitizeHTML } from '../internal/sanitize';\nimport type { ColumnConfig, ColumnState, GridConfig, GridIcons, GridPlugin, IconValue, PluginNameMap } from '../types';\nimport { DEFAULT_GRID_ICONS } from '../types';\n\n// Re-export shared plugin types for convenience\nexport type {\n AfterCellRenderContext,\n AfterRowRenderContext,\n CellClickEvent,\n CellCoords,\n CellEditor,\n CellMouseEvent,\n CellRenderer,\n ContextMenuItem,\n ContextMenuParams,\n GridElementRef,\n HeaderClickEvent,\n HeaderRenderer,\n KeyboardModifiers,\n PluginCellRenderContext,\n PluginQuery,\n RowClickEvent,\n ScrollEvent,\n} from './types';\n\nimport type {\n AfterCellRenderContext,\n AfterRowRenderContext,\n CellClickEvent,\n CellEditor,\n CellMouseEvent,\n CellRenderer,\n GridElementRef,\n HeaderClickEvent,\n HeaderRenderer,\n PluginQuery,\n RowClickEvent,\n ScrollEvent,\n} from './types';\n\n/**\n * Grid element interface for plugins.\n * Extends GridElementRef with plugin-specific methods.\n * Note: effectiveConfig is already available from GridElementRef.\n */\nexport interface GridElement extends GridElementRef {\n getPlugin<T extends BaseGridPlugin>(PluginClass: new (...args: any[]) => T): T | undefined;\n getPluginByName<K extends string>(\n name: K,\n ): (K extends keyof PluginNameMap ? PluginNameMap[K] : BaseGridPlugin) | undefined;\n /**\n * Get a plugin's state by plugin name.\n * This is a loose-coupling method for plugins to access other plugins' state\n * without importing the plugin class directly.\n * @internal Plugin API\n */\n getPluginState?(name: string): unknown;\n\n // Focus Management\n /** Register an external DOM element as a logical focus container. @internal Plugin API */\n registerExternalFocusContainer?(el: Element): void;\n /** Unregister a previously registered external focus container. @internal Plugin API */\n unregisterExternalFocusContainer?(el: Element): void;\n /** Check whether focus is logically inside this grid (own DOM + external containers). @internal Plugin API */\n containsFocus?(node?: Node | null): boolean;\n}\n\n/**\n * Header render context for plugin header renderers.\n */\nexport interface PluginHeaderRenderContext {\n /** Column configuration */\n column: ColumnConfig;\n /** Column index */\n colIndex: number;\n}\n\n// ============================================================================\n// Plugin Dependency Types\n// ============================================================================\n\n/**\n * Declares a dependency on another plugin.\n *\n * @category Plugin Development\n * @example\n * ```typescript\n * export class UndoRedoPlugin extends BaseGridPlugin {\n * static readonly dependencies: PluginDependency[] = [\n * { name: 'editing', required: true },\n * ];\n * }\n * ```\n * @since 0.4.1\n */\nexport interface PluginDependency {\n /**\n * The name of the required plugin (matches the plugin's `name` property).\n * Use string names for loose coupling - avoids static imports.\n */\n name: string;\n\n /**\n * Whether this dependency is required (hard) or optional (soft).\n * - `true`: Plugin cannot function without it. Throws error if missing.\n * - `false`: Plugin works with reduced functionality. Silent if missing\n * (set {@link severity} to `'warn'` or `'info'` to opt into a message).\n * @default true\n */\n required?: boolean;\n\n /**\n * Explicit severity when this dependency is missing. Overrides the default\n * derived from {@link required}.\n * - `'error'`: throw (halts grid setup). Default when `required` is not `false`.\n * - `'warn'`: log a console warning and continue (development only).\n * - `'info'`: log a verbose console.debug message and continue (development only).\n *\n * When omitted, a missing hard dependency throws and a missing soft\n * dependency is silent — preserving backward-compatible behavior.\n * @since 2.16.0\n */\n severity?: 'error' | 'warn' | 'info';\n\n /**\n * Optional predicate that gates whether this dependency applies, evaluated\n * against the depending plugin's own resolved configuration (defaults merged\n * with user config). Return `false` to skip the dependency entirely.\n *\n * Lets a plugin require/recommend another plugin only under certain config\n * — e.g. a plugin that needs a tool-panel host only when its tool panel is\n * enabled. When omitted, the dependency always applies.\n * @example when: (cfg: PivotConfig) => cfg.showToolPanel === true\n * @since 2.16.0\n */\n when?: (pluginConfig: unknown) => boolean;\n\n /**\n * Human-readable reason for this dependency.\n * Shown in error/info messages to help users understand why.\n * @example \"UndoRedoPlugin needs EditingPlugin to track cell edits\"\n */\n reason?: string;\n}\n\n/**\n * Declares an incompatibility between plugins.\n * When both plugins are loaded, a warning is logged to help users understand the conflict.\n *\n * @category Plugin Development\n */\nexport interface PluginIncompatibility {\n /**\n * The name of the incompatible plugin (matches the plugin's `name` property).\n */\n name: string;\n\n /**\n * Human-readable reason for the incompatibility.\n * Should explain why the plugins conflict and any workarounds.\n * @example \"Both transform the entire row model in different ways\"\n */\n reason: string;\n}\n\n// ============================================================================\n// Plugin Query & Event Definitions\n// ============================================================================\n\n/**\n * Defines a query that a plugin can handle.\n * Other plugins or the grid can send this query type to retrieve data.\n *\n * @category Plugin Development\n *\n * @example\n * ```typescript\n * // In manifest\n * queries: [\n * {\n * type: 'canMoveColumn',\n * description: 'Check if a column can be moved/reordered',\n * },\n * ]\n *\n * // In plugin class\n * handleQuery(query: PluginQuery): unknown {\n * if (query.type === 'canMoveColumn') {\n * return this.canMoveColumn(query.context);\n * }\n * }\n * ```\n * @since 1.8.0\n */\nexport interface QueryDefinition {\n /**\n * The query type identifier (e.g., 'canMoveColumn', 'getContextMenuItems').\n * Should be unique across all plugins.\n */\n type: string;\n\n /**\n * Human-readable description of what the query does.\n */\n description?: string;\n}\n\n/**\n * Defines an event that a plugin can emit.\n * Other plugins can subscribe to these events via the Event Bus.\n *\n * @category Plugin Development\n *\n * @example\n * ```typescript\n * // In manifest\n * events: [\n * {\n * type: 'filter-change',\n * description: 'Emitted when filter criteria change',\n * },\n * ]\n *\n * // In plugin class - emit\n * this.emitPluginEvent('filter-change', { field: 'name', value: 'Alice' });\n *\n * // In another plugin - subscribe\n * this.on('filter-change', (detail) => console.log('Filter changed:', detail));\n * ```\n * @since 1.8.0\n */\nexport interface EventDefinition {\n /**\n * The event type identifier (e.g., 'filter-change', 'selection-change').\n * Used when emitting and subscribing to events.\n */\n type: string;\n\n /**\n * Human-readable description of what the event represents.\n */\n description?: string;\n\n /**\n * Whether this event is cancelable via `event.preventDefault()`.\n * @default false\n */\n cancelable?: boolean;\n}\n\n// ============================================================================\n// Plugin Manifest Types\n// ============================================================================\n\n/**\n * Defines a property that a plugin \"owns\" - used for configuration validation.\n * When this property is used without the owning plugin loaded, an error is thrown.\n *\n * @category Plugin Development\n */\nexport interface PluginPropertyDefinition {\n /** The property name on column or grid config */\n property: string;\n /** Whether this is a column-level or config-level property */\n level: 'column' | 'config';\n /** Human-readable description for error messages (e.g., 'the \"editable\" column property') */\n description: string;\n /** Import path hint for error messages (e.g., \"import { EditingPlugin } from '@toolbox-web/grid/plugins/editing';\") */\n importHint?: string;\n /** Custom check for whether property is considered \"used\" (default: truthy value check) */\n isUsed?: (value: unknown) => boolean;\n}\n\n/**\n * A configuration validation rule for detecting invalid/conflicting settings.\n * Plugins declare rules in their manifest; the validator executes them during grid initialization.\n *\n * @category Plugin Development\n * @template TConfig - The plugin's configuration type\n */\nexport interface PluginConfigRule<TConfig = unknown> {\n /** Rule identifier for debugging (e.g., 'selection/range-dblclick') */\n id: string;\n /** Severity: 'error' throws, 'warn' logs warning */\n severity: 'error' | 'warn';\n /** Human-readable message shown when rule is violated */\n message: string;\n /** Predicate returning true if the rule is VIOLATED (i.e., config is invalid) */\n check: (pluginConfig: TConfig) => boolean;\n}\n\n/**\n * Hook names that can have execution priority configured.\n *\n * @category Plugin Development\n */\nexport type HookName =\n | 'processColumns'\n | 'processConfig'\n | 'processRows'\n | 'afterStructuralRender'\n | 'afterRender'\n | 'afterCellRender'\n | 'afterRowRender'\n | 'onCellClick'\n | 'onCellMouseDown'\n | 'onCellMouseMove'\n | 'onCellMouseUp'\n | 'onHeaderClick'\n | 'onKeyDown'\n | 'onRowClick'\n | 'onScroll'\n | 'onScrollRender';\n\n/**\n * Static metadata about a plugin's capabilities and requirements.\n * Declared as a static property on plugin classes.\n *\n * @category Plugin Development\n * @template TConfig - The plugin's configuration type\n *\n * @example\n * ```typescript\n * export class MyPlugin extends BaseGridPlugin<MyConfig> {\n * static override readonly manifest: PluginManifest<MyConfig> = {\n * ownedProperties: [\n * { property: 'myProp', level: 'column', description: 'the \"myProp\" column property' },\n * ],\n * configRules: [\n * { id: 'my-plugin/invalid-combo', severity: 'warn', message: '...', check: (c) => c.a && c.b },\n * ],\n * };\n * readonly name = 'myPlugin';\n * }\n * ```\n * @since 1.1.0\n */\nexport interface PluginManifest<TConfig = unknown> {\n /**\n * Properties this plugin owns - validated by validate-config.ts.\n * If a user uses one of these properties without loading the plugin, an error is thrown.\n */\n ownedProperties?: PluginPropertyDefinition[];\n\n /**\n * Hook execution priority (higher = later, default 0).\n * Use negative values to run earlier, positive to run later.\n */\n hookPriority?: Partial<Record<HookName, number>>;\n\n /**\n * Configuration validation rules - checked during grid initialization.\n * Rules with severity 'error' throw, 'warn' logs to console.\n */\n configRules?: PluginConfigRule<TConfig>[];\n\n /**\n * Plugins that are incompatible with this plugin.\n * When both plugins are loaded together, a warning is shown.\n *\n * @example\n * ```typescript\n * incompatibleWith: [\n * { name: 'tree', reason: 'Both transform the entire row model in different ways' },\n * ],\n * ```\n */\n incompatibleWith?: PluginIncompatibility[];\n\n /**\n * Queries this plugin can handle.\n * Declares what query types this plugin responds to via `handleQuery()`.\n * This replaces the centralized PLUGIN_QUERIES approach with manifest-declared queries.\n *\n * @example\n * ```typescript\n * queries: [\n * { type: 'canMoveColumn', description: 'Check if a column can be moved' },\n * ],\n * ```\n */\n queries?: QueryDefinition[];\n\n /**\n * Events this plugin can emit.\n * Declares what event types other plugins can subscribe to via `on()`.\n *\n * @example\n * ```typescript\n * events: [\n * { type: 'filter-change', description: 'Emitted when filter criteria change' },\n * ],\n * ```\n */\n events?: EventDefinition[];\n\n /**\n * Whether this plugin's `processRows` hook injects or removes rows\n * (group headers, tree nodes, pivot aggregates, placeholders, etc.).\n *\n * When true, the core sorting module delegates to the render scheduler\n * instead of sorting `_rows` in-place, because `_rows` may contain\n * plugin-generated marker objects that would be corrupted by a direct sort.\n *\n * Plugins that only **filter** or **reorder** existing rows (e.g. Filtering,\n * MultiSort) should leave this unset or set it to `false`.\n */\n modifiesRowStructure?: boolean;\n}\n\n/** Convert camelCase icon key to kebab-case data-icon attribute value. */\nexport function toIconAttr(key: string): string {\n return key.replace(/([A-Z])/g, '-$1').toLowerCase();\n}\n\n/**\n * Abstract base class for all grid plugins.\n *\n * @category Plugin Development\n * @template TConfig - Configuration type for the plugin\n * @since 0.1.1\n */\nexport abstract class BaseGridPlugin<TConfig = unknown> implements GridPlugin {\n /**\n * Plugin dependencies - declare other plugins this one requires.\n *\n * Dependencies are validated when the plugin is attached.\n * Required dependencies throw an error if missing. Optional dependencies are\n * silent by default; set a {@link PluginDependency.severity} of `'warn'` or\n * `'info'` to opt into a development-only log.\n *\n * @example\n * ```typescript\n * static readonly dependencies: PluginDependency[] = [\n * { name: 'editing', required: true, reason: 'Tracks cell edits for undo/redo' },\n * { name: 'selection', required: false, reason: 'Enables selection-based undo' },\n * ];\n * ```\n */\n static readonly dependencies?: PluginDependency[];\n\n /**\n * Plugin manifest - declares owned properties, config rules, and hook priorities.\n *\n * This is read by the configuration validator to:\n * - Validate that required plugins are loaded when their properties are used\n * - Execute configRules to detect invalid/conflicting settings\n * - Order hook execution based on priority\n *\n * @example\n * ```typescript\n * static override readonly manifest: PluginManifest<MyConfig> = {\n * ownedProperties: [\n * { property: 'myProp', level: 'column', description: 'the \"myProp\" column property' },\n * ],\n * configRules: [\n * { id: 'myPlugin/conflict', severity: 'warn', message: '...', check: (c) => c.a && c.b },\n * ],\n * };\n * ```\n */\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n static readonly manifest?: PluginManifest<any>;\n\n /** Unique plugin identifier (derived from class name by default) */\n abstract readonly name: string;\n\n /**\n * Alternative names for backward compatibility.\n * `getPluginByName()` matches against both `name` and `aliases`.\n * @internal\n */\n readonly aliases?: readonly string[];\n\n /**\n * Plugin version - defaults to grid version for built-in plugins.\n * Third-party plugins can override with their own semver.\n */\n readonly version: string = typeof __GRID_VERSION__ !== 'undefined' ? __GRID_VERSION__ : 'dev';\n\n /** CSS styles to inject via `document.adoptedStyleSheets` */\n readonly styles?: string;\n\n /** Custom cell renderers keyed by type name */\n readonly cellRenderers?: Record<string, CellRenderer>;\n\n /** Custom header renderers keyed by type name */\n readonly headerRenderers?: Record<string, HeaderRenderer>;\n\n /** Custom cell editors keyed by type name */\n readonly cellEditors?: Record<string, CellEditor>;\n\n /** The grid instance this plugin is attached to */\n protected grid!: GridElement;\n\n /** Plugin configuration - merged with defaults in attach() */\n protected config!: TConfig;\n\n /** User-provided configuration from constructor */\n protected readonly userConfig: Partial<TConfig>;\n\n /**\n * Plugin-level AbortController for event listener cleanup.\n * Created fresh in attach(), aborted in detach().\n * This ensures event listeners are properly cleaned up when plugins are re-attached.\n */\n #abortController?: AbortController;\n\n /**\n * Default configuration - subclasses should override this getter.\n * Note: This must be a getter (not property initializer) for proper inheritance\n * since property initializers run after parent constructor.\n */\n protected get defaultConfig(): Partial<TConfig> {\n return {};\n }\n\n constructor(config: Partial<TConfig> = {}) {\n this.userConfig = config;\n }\n\n /**\n * Effective configuration (defaults merged with user config), resolved\n * eagerly so it is available **before** {@link attach} runs. Dependency\n * validation happens before `attach()` merges {@link config}, so config\n * -conditional dependency `when` predicates evaluate against this getter.\n *\n * While attached, returns the already-merged {@link config}; otherwise\n * recomputes `defaultConfig` + `userConfig` fresh. `config` is intentionally\n * **not** trusted when detached because {@link detach} does not clear it — a\n * re-validated plugin would otherwise see stale config from a prior\n * attachment. The {@link attach}-managed abort controller is the\n * attached/detached signal.\n * @internal Plugin infrastructure (used by `validatePluginDependencies`).\n */\n get resolvedConfig(): TConfig {\n return (this.#abortController ? this.config : { ...this.defaultConfig, ...this.userConfig }) as TConfig;\n }\n\n /**\n * Merge user-supplied configuration from other plugin instances into this\n * plugin's `userConfig`. Used by `PluginManager.attachAll()`'s alias-collapse\n * pre-pass: when a consumer instantiates the same plugin under multiple\n * names (e.g. `RowReorderPlugin` and `RowDragDropPlugin`, which are aliases\n * after V2.x), only one instance is attached, and the other instances'\n * configs are folded in via this method.\n *\n * Merge rules (shallow):\n *\n * - Same key, **same value** → silent.\n * - Same key, **different values** (incl. function vs function) → throws\n * a diagnostic naming the conflicting key. Conflicting callbacks\n * (`canDrag`, `canDrop`, etc.) cannot be reconciled automatically.\n * - **Disjoint keys** → merged cleanly.\n *\n * Subclasses may override to implement custom deep-merge logic, but should\n * call `super.mergeConfigsFrom(others)` to inherit the conflict-detection.\n *\n * @internal Plugin infrastructure (used by `PluginManager.attachAll`).\n * @throws Error with diagnostic code `TBW025` when configs conflict.\n */\n mergeConfigsFrom(others: readonly BaseGridPlugin<TConfig>[]): void {\n if (others.length === 0) return;\n const merged = { ...this.userConfig } as Record<string, unknown>;\n const sources: Record<string, BaseGridPlugin<TConfig>> = {};\n for (const key of Object.keys(merged)) sources[key] = this;\n for (const other of others) {\n const otherConfig = other.userConfig as Record<string, unknown>;\n for (const [key, value] of Object.entries(otherConfig)) {\n if (value === undefined) continue;\n if (!(key in merged)) {\n merged[key] = value;\n sources[key] = other;\n continue;\n }\n if (merged[key] === value) continue;\n // Conflict\n const ownerA = sources[key]?.constructor.name ?? this.constructor.name;\n const ownerB = other.constructor.name;\n const code = formatDiagnostic(\n PLUGIN_ALIAS_CONFIG_CONFLICT,\n `Cannot merge plugin configs for \"${this.name}\": conflicting value for \"${key}\" supplied by both ${ownerA} and ${ownerB}. Pass the option on a single instance, or remove the duplicate.`,\n undefined,\n this.name,\n );\n throw new Error(code);\n }\n }\n // Mutate userConfig in place so re-attach picks it up via attach().\n // `userConfig` is declared `readonly` to prevent reassigning the binding,\n // but the object's own properties are intentionally mutable for this\n // alias-collapse pre-pass. A single cast to a record is enough — no\n // `as unknown as` double-cast needed.\n Object.assign(this.userConfig as Record<string, unknown>, merged);\n }\n\n /**\n * Replace this plugin's `userConfig` with a freshly-resolved sibling\n * instance's config. Used by the grid's per-grid feature-instance gate\n * (`#initializePlugins`): when a feature is re-resolved across a config\n * change, the **existing** instance is reused — so its accumulated state\n * survives — but it must adopt the latest `gridConfig.features.<name>`\n * value. Unlike {@link mergeConfigsFrom}, this is last-write-wins with no\n * conflict detection: the re-resolved instance is authoritative.\n *\n * `attach()` recomputes {@link config} from `userConfig`, so updating\n * `userConfig` here is enough for the reused instance to pick up the new\n * config on its next attach during the same `#initializePlugins` pass.\n *\n * @internal Plugin infrastructure (used by the grid feature gate).\n */\n refreshUserConfigFrom(other: BaseGridPlugin<TConfig>): void {\n const target = this.userConfig as Record<string, unknown>;\n // Snapshot the source BEFORE clearing `target`. A feature factory may\n // store the consumer's config object by reference (e.g. `new\n // ContextMenuPlugin(config)`), and a consumer that passes the *same* config\n // object across re-resolutions (common when `gridConfig` is a recomputed\n // Angular `computed()`) makes `this.userConfig === other.userConfig`.\n // Without this snapshot, the `delete` loop below would empty that shared\n // object first, then `Object.assign` would copy back nothing — silently\n // wiping the config and falling back to plugin defaults. (#contextMenu)\n const source = { ...(other.userConfig as Record<string, unknown>) };\n for (const key of Object.keys(target)) delete target[key];\n Object.assign(target, source);\n }\n\n /**\n * Called when the plugin is attached to a grid.\n * Override to set up event listeners, initialize state, etc.\n *\n * @example\n * ```ts\n * attach(grid: GridElement): void {\n * super.attach(grid);\n * // Set up document-level listeners with auto-cleanup\n * document.addEventListener('keydown', this.handleEscape, {\n * signal: this.disconnectSignal\n * });\n * }\n * ```\n */\n attach(grid: GridElement): void {\n // Abort any previous abort controller (in case of re-attach without detach)\n this.#abortController?.abort();\n // Create fresh abort controller for this attachment\n this.#abortController = new AbortController();\n\n this.grid = grid;\n // Merge config here (after subclass construction is complete)\n this.config = { ...this.defaultConfig, ...this.userConfig } as TConfig;\n }\n\n /**\n * Called when the plugin is detached from a grid.\n * Override to clean up event listeners, timers, etc.\n *\n * @example\n * ```ts\n * detach(): void {\n * // Clean up any state not handled by disconnectSignal\n * this.selectedRows.clear();\n * this.cache = null;\n * }\n * ```\n */\n detach(): void {\n // Abort the plugin's abort controller to clean up all event listeners\n // registered with { signal: this.disconnectSignal }\n this.#abortController?.abort();\n this.#abortController = undefined;\n // Override in subclass for additional cleanup\n }\n\n /**\n * Called when another plugin is attached to the same grid.\n * Use for inter-plugin coordination, e.g., to integrate with new plugins.\n *\n * @param name - The name of the plugin that was attached\n * @param plugin - The plugin instance (for direct access if needed)\n *\n * @example\n * ```ts\n * onPluginAttached(name: string, plugin: BaseGridPlugin): void {\n * if (name === 'selection') {\n * // Integrate with selection plugin\n * this.selectionPlugin = plugin as SelectionPlugin;\n * }\n * }\n * ```\n */\n onPluginAttached?(name: string, plugin: BaseGridPlugin): void;\n\n /**\n * Called when another plugin is detached from the same grid.\n * Use to clean up inter-plugin references.\n *\n * @param name - The name of the plugin that was detached\n *\n * @example\n * ```ts\n * onPluginDetached(name: string): void {\n * if (name === 'selection') {\n * this.selectionPlugin = undefined;\n * }\n * }\n * ```\n */\n onPluginDetached?(name: string): void;\n\n /**\n * Get another plugin instance from the same grid.\n * Use for inter-plugin communication.\n *\n * **Prefer {@link BaseGridPlugin.grid grid.getPluginByName()}** when you don't need the class import.\n *\n * @example\n * ```ts\n * // Preferred: by name\n * const selection = this.grid?.getPluginByName('selection');\n *\n * // Alternative: by class\n * const selection = this.getPlugin(SelectionPlugin);\n * if (selection) {\n * const selectedRows = selection.getSelectedRows();\n * }\n * ```\n */\n protected getPlugin<T extends BaseGridPlugin>(PluginClass: new (...args: any[]) => T): T | undefined {\n return this.grid?.getPlugin(PluginClass);\n }\n\n /**\n * Emit a custom event from the grid.\n */\n protected emit<T>(eventName: string, detail: T): void {\n this.grid?.dispatchEvent?.(new CustomEvent(eventName, { detail, bubbles: true }));\n }\n\n /**\n * Emit a cancelable custom event from the grid.\n * @returns `true` if the event was cancelled (preventDefault called), `false` otherwise\n */\n protected emitCancelable<T>(eventName: string, detail: T): boolean {\n const event = new CustomEvent(eventName, { detail, bubbles: true, cancelable: true });\n this.grid?.dispatchEvent?.(event);\n return event.defaultPrevented;\n }\n\n // =========================================================================\n // Event Bus - Plugin-to-Plugin Communication\n // =========================================================================\n\n /**\n * Subscribe to an event from another plugin.\n * The subscription is automatically cleaned up when this plugin is detached.\n *\n * @category Plugin Development\n * @param eventType - The event type to listen for (e.g., 'filter-change')\n * @param callback - The callback to invoke when the event is emitted\n *\n * @example\n * ```typescript\n * // In attach() or other initialization\n * this.on('filter-change', (detail) => {\n * console.log('Filter changed:', detail);\n * });\n * ```\n */\n protected on<T = unknown>(eventType: string, callback: (detail: T) => void): void {\n this.grid?._pluginManager?.subscribe(this, eventType, callback as (detail: unknown) => void);\n }\n\n /**\n * Unsubscribe from a plugin event.\n *\n * @category Plugin Development\n * @param eventType - The event type to stop listening for\n *\n * @example\n * ```typescript\n * this.off('filter-change');\n * ```\n */\n protected off(eventType: string): void {\n this.grid?._pluginManager?.unsubscribe(this, eventType);\n }\n\n /**\n * Emit an event to other plugins via the Event Bus.\n * This is for inter-plugin communication only; it does NOT dispatch DOM events.\n * Use `emit()` to dispatch DOM events that external consumers can listen to.\n *\n * @category Plugin Development\n * @param eventType - The event type to emit (should be declared in manifest.events)\n * @param detail - The event payload\n *\n * @example\n * ```typescript\n * // Emit to other plugins (not DOM)\n * this.emitPluginEvent('filter-change', { field: 'name', value: 'Alice' });\n *\n * // For DOM events that consumers can addEventListener to:\n * this.emit('filter-change', { field: 'name', value: 'Alice' });\n * ```\n */\n protected emitPluginEvent<T>(eventType: string, detail: T): void {\n this.grid?._pluginManager?.emitPluginEvent(eventType, detail);\n }\n\n /**\n * Emit an event to **both** the plugin Event Bus (for inter-plugin communication)\n * **and** the DOM (for external consumers via `addEventListener`).\n *\n * Use this when a state change is relevant to both other plugins and external\n * consumers. For example, `sort-change` needs to invalidate Selection (plugin bus)\n * and notify the host application (DOM event).\n *\n * @category Plugin Development\n * @param eventType - The event type to broadcast\n * @param detail - The event payload\n *\n * @example\n * ```typescript\n * // Notify both plugins and consumers of a sort change\n * this.broadcast('sort-change', { sortModel: [...this.sortModel] });\n * ```\n */\n protected broadcast<T>(eventType: string, detail: T): void {\n this.emitPluginEvent(eventType, detail);\n this.emit(eventType, detail);\n }\n\n /**\n * Request a re-render of the grid.\n * Uses ROWS phase - does NOT trigger processColumns hooks.\n */\n protected requestRender(): void {\n this.grid?.requestRender?.();\n }\n\n /**\n * Request a columns re-render of the grid.\n * Uses COLUMNS phase - triggers processColumns hooks.\n * Use this when your plugin needs to reprocess column configuration.\n */\n protected requestColumnsRender(): void {\n (this.grid as { requestColumnsRender?: () => void })?.requestColumnsRender?.();\n }\n\n /**\n * Request a re-render and restore focus styling afterward.\n * Use this when a plugin action (like expand/collapse) triggers a render\n * but needs to maintain keyboard navigation focus.\n */\n protected requestRenderWithFocus(): void {\n this.grid?.requestRenderWithFocus?.();\n }\n\n /**\n * Request a lightweight style update without rebuilding DOM.\n * Use this instead of requestRender() when only CSS classes need updating.\n */\n protected requestAfterRender(): void {\n this.grid?.requestAfterRender?.();\n }\n\n /**\n * Re-render visible rows without rebuilding the row model or recalculating geometry.\n * Use this when row data has been updated in-place (e.g., server-side block loads)\n * and only the visible viewport needs to re-render.\n */\n protected requestVirtualRefresh(): void {\n this.grid?.requestVirtualRefresh?.();\n }\n\n /**\n * Get the current rows from the grid.\n */\n protected get rows(): any[] {\n return this.grid?.rows ?? [];\n }\n\n /**\n * Get the original unfiltered/unprocessed rows from the grid.\n * Use this when you need all source data regardless of active filters.\n */\n protected get sourceRows(): any[] {\n return this.grid?.sourceRows ?? [];\n }\n\n /**\n * Get the current columns from the grid.\n */\n protected get columns(): ColumnConfig[] {\n return this.grid?.columns ?? [];\n }\n\n /**\n * Get only visible columns from the grid (excludes hidden).\n * Use this for rendering that needs to match the grid template.\n */\n protected get visibleColumns(): ColumnConfig[] {\n return this.grid?._visibleColumns ?? [];\n }\n\n /**\n * Get the grid as an HTMLElement for direct DOM operations.\n * Use sparingly - prefer the typed GridElementRef API when possible.\n *\n * @example\n * ```ts\n * const width = this.gridElement.clientWidth;\n * this.gridElement.classList.add('my-plugin-active');\n * ```\n */\n protected get gridElement(): HTMLElement {\n return this.grid?._hostElement;\n }\n\n /**\n * Get the disconnect signal for event listener cleanup.\n * This signal is aborted when the grid disconnects from the DOM.\n * Use this when adding event listeners that should be cleaned up automatically.\n *\n * Best for:\n * - Document/window-level listeners added in attach()\n * - Listeners on the grid element itself\n * - Any listener that should persist across renders\n *\n * Not needed for:\n * - Listeners on elements created in afterRender() (removed with element)\n *\n * @example\n * element.addEventListener('click', handler, { signal: this.disconnectSignal });\n * document.addEventListener('keydown', handler, { signal: this.disconnectSignal });\n */\n protected get disconnectSignal(): AbortSignal {\n // Return the plugin's own abort signal for proper cleanup on detach/re-attach\n // Falls back to grid's signal if plugin's controller not yet created\n return this.#abortController?.signal ?? this.grid?.disconnectSignal;\n }\n\n /**\n * Get the grid-level icons configuration.\n * Returns merged icons (user config + defaults).\n */\n protected get gridIcons(): Required<GridIcons> {\n const userIcons = this.grid?.gridConfig?.icons ?? {};\n return { ...DEFAULT_GRID_ICONS, ...userIcons };\n }\n\n // #region Animation Helpers\n\n /**\n * Check if animations are enabled at the grid level.\n * Respects gridConfig.animation.mode and the CSS variable set by the grid.\n *\n * Plugins should use this to skip animations when:\n * - Animation mode is 'off' or `false`\n * - User prefers reduced motion and mode is 'reduced-motion' (default)\n *\n * @example\n * ```ts\n * private get animationStyle(): 'slide' | 'fade' | false {\n * if (!this.isAnimationEnabled) return false;\n * return this.config.animation ?? 'slide';\n * }\n * ```\n */\n protected get isAnimationEnabled(): boolean {\n const mode = this.grid?.effectiveConfig?.animation?.mode ?? 'reduced-motion';\n\n // Explicit off = disabled\n if (mode === false || mode === 'off') return false;\n\n // Explicit on = always enabled\n if (mode === true || mode === 'on') return true;\n\n // reduced-motion: check CSS variable (set by grid based on media query)\n const host = this.gridElement;\n if (host) {\n const enabled = getComputedStyle(host).getPropertyValue('--tbw-animation-enabled').trim();\n return enabled !== '0';\n }\n\n return true; // Default to enabled\n }\n\n /**\n * Get the animation duration in milliseconds from CSS variable.\n * Falls back to 200ms if not set.\n *\n * Plugins can use this for their animation timing to stay consistent\n * with the grid-level animation.duration setting.\n *\n * @example\n * ```ts\n * element.animate(keyframes, { duration: this.animationDuration });\n * ```\n */\n protected get animationDuration(): number {\n const host = this.gridElement;\n if (host) {\n const durationStr = getComputedStyle(host).getPropertyValue('--tbw-animation-duration').trim();\n const parsed = parseInt(durationStr, 10);\n if (!isNaN(parsed)) return parsed;\n }\n return 200; // Default\n }\n\n // #endregion\n\n /**\n * Set an icon on an element using the CSS-first hybrid approach.\n *\n * Sets `data-icon` for CSS pseudo-element rendering and `data-expanded` for\n * expand/collapse toggle elements. Only injects DOM content when a JS override\n * is present (via `gridConfig.icons` or `pluginOverride`), which naturally\n * suppresses the CSS `::before` via the `:empty` selector.\n *\n * @param element - The element to set the icon on\n * @param iconKey - The icon key in GridIcons (e.g., 'expand', 'collapse')\n * @param pluginOverride - Optional plugin-level override (string or HTMLElement)\n */\n protected setIcon(element: HTMLElement, iconKey: keyof GridIcons, pluginOverride?: IconValue): void {\n // Set data-icon attribute for CSS targeting (always)\n element.dataset.icon = toIconAttr(iconKey);\n\n // Set data-expanded for toggle elements (tree, group, master-detail)\n if (iconKey === 'collapse') {\n element.dataset.expanded = '';\n } else if (iconKey === 'expand') {\n delete element.dataset.expanded;\n }\n\n // Resolve JS-only icon (returns undefined when CSS default should be used)\n const jsIcon = this.#resolveJsIcon(iconKey, pluginOverride);\n\n if (jsIcon !== undefined) {\n // JS override: inject content (suppresses CSS ::before via :empty)\n if (typeof jsIcon === 'string') {\n element.innerHTML = sanitizeHTML(jsIcon);\n } else if (jsIcon instanceof HTMLElement) {\n element.innerHTML = '';\n element.appendChild(jsIcon.cloneNode(true));\n }\n } else {\n // CSS path: ensure element is empty so ::before renders\n element.innerHTML = '';\n }\n }\n\n /**\n * Resolve JS-only icon override. Returns undefined if CSS default should render.\n * Only returns a value when the user explicitly set gridConfig.icons[key] or\n * the plugin passed an override.\n */\n #resolveJsIcon(iconKey: keyof GridIcons, pluginOverride?: IconValue): IconValue | undefined {\n if (pluginOverride !== undefined) return pluginOverride;\n return this.grid?.gridConfig?.icons?.[iconKey];\n }\n\n /**\n * Create or replace a sort indicator on a header cell.\n *\n * Handles the full lifecycle: removes any existing indicator, creates a new\n * `<span part=\"sort-indicator\" class=\"sort-indicator\">` with the appropriate\n * icon, sets `aria-sort` / `data-sort` attributes, and inserts it before the\n * filter button or resize handle.\n *\n * Plugins decide **which** cells and **when** — this method handles **how**.\n *\n * @param cell - The header cell element\n * @param direction - `'asc'` | `'desc'` for sorted, `null` for unsorted idle state\n * @returns The created indicator element (useful for appending badges, etc.)\n */\n protected updateSortIndicator(cell: Element, direction: 'asc' | 'desc' | null): HTMLElement {\n // Remove any existing indicator (core uses part, plugins use class)\n cell.querySelector('[part~=\"sort-indicator\"], .sort-indicator')?.remove();\n\n const indicator = document.createElement('span');\n indicator.setAttribute('part', 'sort-indicator');\n indicator.className = 'sort-indicator';\n\n if (direction) {\n cell.setAttribute('aria-sort', direction === 'asc' ? 'ascending' : 'descending');\n cell.setAttribute('data-sort', direction);\n this.setIcon(indicator, direction === 'asc' ? 'sortAsc' : 'sortDesc');\n } else {\n cell.setAttribute('aria-sort', 'none');\n cell.removeAttribute('data-sort');\n this.setIcon(indicator, 'sortNone');\n }\n\n // Insert before filter button or resize handle to maintain DOM order\n const insertBefore = cell.querySelector('.tbw-filter-btn') ?? cell.querySelector('.resize-handle');\n if (insertBefore) {\n cell.insertBefore(indicator, insertBefore);\n } else {\n cell.appendChild(indicator);\n }\n\n return indicator;\n }\n\n /**\n * Log a warning with an optional diagnostic code.\n *\n * When a diagnostic code is provided, the message is formatted with the code\n * and a link to the troubleshooting docs.\n *\n * @example\n * ```ts\n * this.warn('Something went wrong'); // plain\n * this.warn(MISSING_BREAKPOINT, 'Set a breakpoint'); // with code + docs link\n * ```\n */\n protected warn(message: string): void;\n protected warn(code: DiagnosticCode, message: string): void;\n protected warn(codeOrMessage: DiagnosticCode | string, message?: string): void {\n if (message !== undefined) {\n // Called with (code, message)\n console.warn(formatDiagnostic(codeOrMessage as DiagnosticCode, message, this.gridElement.id, this.name));\n } else {\n // Called with (message) — plain warning, no diagnostic code\n console.warn(`${gridPrefix(this.gridElement.id, this.name)} ${codeOrMessage}`);\n }\n }\n\n /**\n * Throw an error with a diagnostic code and docs link.\n * Use for configuration errors and API misuse that should halt execution.\n */\n protected throwDiagnostic(code: DiagnosticCode, message: string): never {\n throw new Error(formatDiagnostic(code, message, this.gridElement.id, this.name));\n }\n\n // #region Lifecycle Hooks\n\n /**\n * Transform rows before rendering.\n * Called during each render cycle before rows are rendered to the DOM.\n * Use this to filter, sort, or add computed properties to rows.\n *\n * @param rows - The current rows array (readonly to encourage returning a new array)\n * @returns The modified rows array to render\n *\n * @example\n * ```ts\n * processRows(rows: readonly any[]): any[] {\n * // Filter out hidden rows\n * return rows.filter(row => !row._hidden);\n * }\n * ```\n *\n * @example\n * ```ts\n * processRows(rows: readonly any[]): any[] {\n * // Add computed properties\n * return rows.map(row => ({\n * ...row,\n * _fullName: `${row.firstName} ${row.lastName}`\n * }));\n * }\n * ```\n */\n processRows?(rows: readonly any[]): any[];\n\n /**\n * Contribute to the grid's effective configuration during each config merge.\n * Called by {@link ConfigManager.merge} after the base config is assembled,\n * before it becomes `effectiveConfig`. Mutate `config` in place to inject\n * plugin-derived settings (e.g. the shell plugin folds its light-DOM /\n * API-registered state into `config.shell`).\n *\n * This is the config-pipeline counterpart to {@link processColumns} /\n * {@link processRows}: it keeps the core config merge free of any plugin\n * knowledge — each plugin owns the shape it contributes.\n *\n * @param config - The mutable base config being assembled into effectiveConfig.\n *\n * @example\n * ```ts\n * processConfig(config: GridConfig): void {\n * config.shell = { ...config.shell, header: { title: this.#title } };\n * }\n * ```\n */\n processConfig?(config: GridConfig): void;\n\n /**\n * Transform columns before rendering.\n * Called during each render cycle before column headers and cells are rendered.\n * Use this to add, remove, or modify column definitions.\n *\n * @param columns - The current columns array (readonly to encourage returning a new array)\n * @returns The modified columns array to render\n *\n * @example\n * ```ts\n * processColumns(columns: readonly ColumnConfig[]): ColumnConfig[] {\n * // Add a selection checkbox column\n * return [\n * { field: '_select', header: '', width: 40 },\n * ...columns\n * ];\n * }\n * ```\n */\n processColumns?(columns: readonly ColumnConfig[]): ColumnConfig[];\n\n /**\n * Called before each render cycle begins.\n * Use this to prepare state or cache values needed during rendering.\n *\n * **Note:** This hook is currently a placeholder for future implementation.\n * It is defined in the interface but not yet invoked by the grid's render pipeline.\n * If you need this functionality, please open an issue or contribute an implementation.\n *\n * @example\n * ```ts\n * beforeRender(): void {\n * this.visibleRowCount = this.calculateVisibleRows();\n * }\n * ```\n */\n beforeRender?(): void;\n\n /**\n * Called after each render cycle completes.\n * Use this for DOM manipulation, adding event listeners to rendered elements,\n * or applying visual effects like selection highlights.\n *\n * @example\n * ```ts\n * afterRender(): void {\n * // Apply selection styling to rendered rows\n * const rows = this.gridElement?.querySelectorAll('.data-grid-row');\n * rows?.forEach((row, i) => {\n * row.classList.toggle('selected', this.selectedRows.has(i));\n * });\n * }\n * ```\n */\n afterRender?(): void;\n\n /**\n * Called synchronously immediately after the grid (re)builds its root DOM\n * structure, before the browser has a chance to paint.\n *\n * Unlike {@link afterRender} (which runs in the scheduler's STYLE phase and\n * may be deferred), this hook fires inside the same task as the structural\n * DOM build. It is the correct place for plugins that need to wrap, relocate,\n * or augment the freshly built grid DOM without a flash of unstyled/unwrapped\n * content — e.g. a shell that moves the grid content into a chrome wrapper.\n *\n * The grid emits this hook every time it performs a full structural rebuild\n * (initial connect and structural changes), but NOT on the scroll/data hot\n * path. Implementations MUST be idempotent: a full rebuild discards any DOM a\n * previous invocation produced, so re-create it, and no-op when the desired\n * structure is already present.\n *\n * @example\n * ```ts\n * afterStructuralRender(): void {\n * const root = this.grid?._renderRoot;\n * if (!root || root.querySelector('.my-wrapper > .tbw-grid-content')) return;\n * // build wrapper, move .tbw-grid-content inside it\n * }\n * ```\n */\n afterStructuralRender?(): void;\n\n /**\n * Called synchronously by the grid immediately before it tears down and\n * rebuilds its root DOM structure (a structural re-render). Plugins that\n * relocated existing light-DOM nodes into their own chrome (e.g. a shell that\n * moves toolbar buttons into the header bar) use this to move those nodes back\n * to their original container so they survive the rebuild. Generic\n * counterpart to {@link afterStructuralRender}. MUST be idempotent.\n */\n beforeStructuralRender?(): void;\n\n /**\n * Called by the grid after a config merge to ask whether the change requires a\n * full structural re-render (rather than a cheaper in-place update). A plugin\n * returns `true` when its rendered DOM signature no longer matches the new\n * effective config — e.g. a shell header appearing/disappearing or a tool\n * panel's count/position/mode changing. The grid performs a structural\n * re-render if ANY plugin returns `true`. MUST be side-effect free.\n */\n needsStructuralRerender?(): boolean;\n\n /**\n * Called by the grid to let a plugin (re-)parse its own light-DOM\n * contributions into plugin state ahead of a config merge. Generic seam that\n * keeps core free of plugin-specific light-DOM tag knowledge (the plugin reads\n * `this.grid._hostElement`). MUST be idempotent.\n */\n parseLightDom?(): void;\n\n /**\n * Called by the grid (from `refreshColumns` and the light-DOM observer) to\n * let a plugin re-parse its light-DOM contributions AND refresh any\n * already-rendered chrome in place when something changed — e.g. a shell\n * title or tool buttons that a framework projected asynchronously after the\n * initial render. Distinct from {@link parseLightDom}, which only updates\n * plugin state. Generic seam — core holds no plugin-specific knowledge. MUST\n * be idempotent and a no-op when the plugin's chrome is not yet rendered.\n */\n syncLightDom?(): void;\n\n /**\n * Called after each cell is rendered.\n * This hook is more efficient than `afterRender()` for cell-level modifications\n * because you receive the cell context directly - no DOM queries needed.\n *\n * Use cases:\n * - Adding selection/highlight classes to specific cells\n * - Injecting badges or decorations\n * - Applying conditional styling based on cell value\n *\n * Performance note: Called for every visible cell during render. Keep implementation fast.\n * This hook is also called during scroll when cells are recycled.\n *\n * @param context - The cell render context with row, column, value, and elements\n *\n * @example\n * ```ts\n * afterCellRender(context: AfterCellRenderContext): void {\n * // Add selection class without DOM queries\n * if (this.isSelected(context.rowIndex, context.colIndex)) {\n * context.cellElement.classList.add('selected');\n * }\n *\n * // Add validation error styling\n * if (this.hasError(context.row, context.column.field)) {\n * context.cellElement.classList.add('has-error');\n * }\n * }\n * ```\n */\n afterCellRender?(context: AfterCellRenderContext): void;\n\n /**\n * Called after a row is fully rendered (all cells complete).\n * Use this for row-level decorations, styling, or ARIA attributes.\n *\n * Common use cases:\n * - Adding selection classes to entire rows (row-focus, selected)\n * - Setting row-level ARIA attributes\n * - Applying row validation highlighting\n * - Tree indentation styling\n *\n * Performance note: Called for every visible row during render. Keep implementation fast.\n * This hook is also called during scroll when rows are recycled.\n *\n * @param context - The row render context with row data and element\n *\n * @example\n * ```ts\n * afterRowRender(context: AfterRowRenderContext): void {\n * // Add row selection class without DOM queries\n * if (this.isRowSelected(context.rowIndex)) {\n * context.rowElement.classList.add('selected', 'row-focus');\n * }\n *\n * // Add validation error styling\n * if (this.rowHasErrors(context.row)) {\n * context.rowElement.classList.add('has-errors');\n * }\n * }\n * ```\n */\n afterRowRender?(context: AfterRowRenderContext): void;\n\n /**\n * Called after scroll-triggered row rendering completes.\n * This is a lightweight hook for applying visual state to recycled DOM elements.\n * Use this instead of afterRender when you need to reapply styling during scroll.\n *\n * Performance note: This is called frequently during scroll. Keep implementation fast.\n *\n * @example\n * ```ts\n * onScrollRender(): void {\n * // Reapply selection state to visible cells\n * this.applySelectionToVisibleCells();\n * }\n * ```\n */\n onScrollRender?(): void;\n\n /**\n * Get the height of a specific row.\n * Used for synthetic rows (group headers, detail panels, etc.) that have fixed heights.\n * Return undefined if this plugin does not manage the height for this row.\n *\n * This hook is called during position cache rebuilds for variable row height virtualization.\n * Plugins that create synthetic rows should implement this to provide accurate heights.\n *\n * @param row - The row data\n * @param index - The row index in the processed rows array\n * @returns The row height in pixels, or undefined if not managed by this plugin\n */\n getRowHeight?(row: unknown, index: number): number | undefined;\n\n /**\n * Adjust the virtualization start index to render additional rows before the visible range.\n * Use this when expanded content (like detail rows) needs its parent row to remain rendered\n * even when the parent row itself has scrolled above the viewport.\n *\n * @param start - The calculated start row index\n * @param scrollTop - The current scroll position\n * @param rowHeight - The height of a single row\n * @returns The adjusted start index (lower than or equal to original start)\n *\n * @example\n * ```ts\n * adjustVirtualStart(start: number, scrollTop: number, rowHeight: number): number {\n * // If row 5 is expanded and scrolled partially, keep it rendered\n * for (const expandedRowIndex of this.expandedRowIndices) {\n * const expandedRowTop = expandedRowIndex * rowHeight;\n * const expandedRowBottom = expandedRowTop + rowHeight + this.detailHeight;\n * if (expandedRowBottom > scrollTop && expandedRowIndex < start) {\n * return expandedRowIndex;\n * }\n * }\n * return start;\n * }\n * ```\n */\n adjustVirtualStart?(start: number, scrollTop: number, rowHeight: number): number;\n\n /**\n * Render a custom row, bypassing the default row rendering.\n * Use this for special row types like group headers, detail rows, or footers.\n *\n * @param row - The row data object\n * @param rowEl - The row DOM element to render into\n * @param rowIndex - The index of the row in the data array\n * @returns `true` if the plugin handled rendering (prevents default), `false`/`void` for default rendering\n *\n * @example\n * ```ts\n * renderRow(row: any, rowEl: HTMLElement, rowIndex: number): boolean | void {\n * if (row._isGroupHeader) {\n * rowEl.innerHTML = `<div class=\"group-header\">${row._groupLabel}</div>`;\n * return true; // Handled - skip default rendering\n * }\n * // Return void to let default rendering proceed\n * }\n * ```\n */\n renderRow?(row: any, rowEl: HTMLElement, rowIndex: number): boolean | void;\n\n // #endregion\n\n // #region Inter-Plugin Communication\n\n /**\n * Handle queries from other plugins or the grid.\n *\n * Queries are declared in `manifest.queries` and dispatched via `grid.query()`.\n * This enables type-safe, discoverable inter-plugin communication.\n *\n * @category Plugin Development\n * @param query - The query object with type and context\n * @returns Query-specific response, or undefined if not handling this query\n *\n * @example\n * ```ts\n * // In manifest\n * static override readonly manifest: PluginManifest = {\n * queries: [\n * { type: 'canMoveColumn', description: 'Check if a column can be moved' },\n * ],\n * };\n *\n * // In plugin class\n * handleQuery(query: PluginQuery): unknown {\n * if (query.type === 'canMoveColumn') {\n * const column = query.context as ColumnConfig;\n * return !column.sticky; // Can't move sticky columns\n * }\n * }\n * ```\n */\n handleQuery?(query: PluginQuery): unknown;\n\n // #endregion\n\n // #region Interaction Hooks\n\n /**\n * Handle keyboard events on the grid.\n * Called when a key is pressed while the grid or a cell has focus.\n *\n * @param event - The native KeyboardEvent\n * @returns `true` to prevent default behavior and stop propagation, `false`/`void` to allow default\n *\n * @example\n * ```ts\n * onKeyDown(event: KeyboardEvent): boolean | void {\n * // Handle Ctrl+A for select all\n * if (event.ctrlKey && event.key === 'a') {\n * this.selectAllRows();\n * return true; // Prevent default browser select-all\n * }\n * }\n * ```\n */\n onKeyDown?(event: KeyboardEvent): boolean | void;\n\n /**\n * Handle cell click events.\n * Called when a data cell is clicked (not headers).\n *\n * @param event - Cell click event with row/column context\n * @returns `true` to prevent default behavior and stop propagation, `false`/`void` to allow default\n *\n * @example\n * ```ts\n * onCellClick(event: CellClickEvent): boolean | void {\n * if (event.field === '_select') {\n * this.toggleRowSelection(event.rowIndex);\n * return true; // Handled\n * }\n * }\n * ```\n */\n onCellClick?(event: CellClickEvent): boolean | void;\n\n /**\n * Handle row click events.\n * Called when any part of a data row is clicked.\n * Note: This is called in addition to onCellClick, not instead of.\n *\n * @param event - Row click event with row context\n * @returns `true` to prevent default behavior and stop propagation, `false`/`void` to allow default\n *\n * @example\n * ```ts\n * onRowClick(event: RowClickEvent): boolean | void {\n * if (this.config.mode === 'row') {\n * this.selectRow(event.rowIndex, event.originalEvent);\n * return true;\n * }\n * }\n * ```\n */\n onRowClick?(event: RowClickEvent): boolean | void;\n\n /**\n * Handle header click events.\n * Called when a column header is clicked. Commonly used for sorting.\n *\n * @param event - Header click event with column context\n * @returns `true` to prevent default behavior and stop propagation, `false`/`void` to allow default\n *\n * @example\n * ```ts\n * onHeaderClick(event: HeaderClickEvent): boolean | void {\n * if (event.column.sortable !== false) {\n * this.toggleSort(event.field);\n * return true;\n * }\n * }\n * ```\n */\n onHeaderClick?(event: HeaderClickEvent): boolean | void;\n\n /**\n * Handle scroll events on the grid viewport.\n * Called during scrolling. Note: This may be called frequently; debounce if needed.\n *\n * @param event - Scroll event with scroll position and viewport dimensions\n *\n * @example\n * ```ts\n * onScroll(event: ScrollEvent): void {\n * // Update sticky column positions\n * this.updateStickyPositions(event.scrollLeft);\n * }\n * ```\n */\n onScroll?(event: ScrollEvent): void;\n\n /**\n * Handle cell mousedown events.\n * Used for initiating drag operations like range selection or column resize.\n *\n * @param event - Mouse event with cell context\n * @returns `true` to indicate drag started (prevents text selection), `false`/`void` otherwise\n *\n * @example\n * ```ts\n * onCellMouseDown(event: CellMouseEvent): boolean | void {\n * if (event.rowIndex !== undefined && this.config.mode === 'range') {\n * this.startDragSelection(event.rowIndex, event.colIndex);\n * return true; // Prevent text selection\n * }\n * }\n * ```\n */\n onCellMouseDown?(event: CellMouseEvent): boolean | void;\n\n /**\n * Handle cell mousemove events during drag operations.\n * Only called when a drag is in progress (after mousedown returned true).\n *\n * @param event - Mouse event with current cell context\n * @returns `true` to continue handling the drag, `false`/`void` otherwise\n *\n * @example\n * ```ts\n * onCellMouseMove(event: CellMouseEvent): boolean | void {\n * if (this.isDragging && event.rowIndex !== undefined) {\n * this.extendSelection(event.rowIndex, event.colIndex);\n * return true;\n * }\n * }\n * ```\n */\n onCellMouseMove?(event: CellMouseEvent): boolean | void;\n\n /**\n * Handle cell mouseup events to end drag operations.\n *\n * @param event - Mouse event with final cell context\n * @returns `true` if drag was finalized, `false`/`void` otherwise\n *\n * @example\n * ```ts\n * onCellMouseUp(event: CellMouseEvent): boolean | void {\n * if (this.isDragging) {\n * this.finalizeDragSelection();\n * this.isDragging = false;\n * return true;\n * }\n * }\n * ```\n */\n onCellMouseUp?(event: CellMouseEvent): boolean | void;\n\n // Note: Context menu items are provided via handleQuery('getContextMenuItems').\n // This keeps the core decoupled from the context-menu plugin specifics.\n\n // #endregion\n\n // #region Column State Hooks\n\n /**\n * Contribute plugin-specific state for a column.\n * Called by the grid when collecting column state for serialization.\n * Plugins can add their own properties to the column state.\n *\n * @param field - The field name of the column\n * @returns Partial column state with plugin-specific properties, or undefined if no state to contribute\n *\n * @example\n * ```ts\n * getColumnState(field: string): Partial<ColumnState> | undefined {\n * const filterModel = this.filterModels.get(field);\n * if (filterModel) {\n * // Uses module augmentation to add filter property to ColumnState\n * return { filter: filterModel } as Partial<ColumnState>;\n * }\n * return undefined;\n * }\n * ```\n */\n getColumnState?(field: string): Partial<ColumnState> | undefined;\n\n /**\n * Apply plugin-specific state to a column.\n * Called by the grid when restoring column state from serialized data.\n * Plugins should restore their internal state based on the provided state.\n *\n * @param field - The field name of the column\n * @param state - The column state to apply (may contain plugin-specific properties)\n *\n * @example\n * ```ts\n * applyColumnState(field: string, state: ColumnState): void {\n * // Check for filter property added via module augmentation\n * const filter = (state as any).filter;\n * if (filter) {\n * this.filterModels.set(field, filter);\n * this.applyFilter();\n * }\n * }\n * ```\n */\n applyColumnState?(field: string, state: ColumnState): void;\n\n // #endregion\n\n // #region Scroll Boundary Hooks\n\n /**\n * Report horizontal scroll boundary offsets for this plugin.\n * Plugins that obscure part of the scroll area (e.g., pinned/sticky columns)\n * should return how much space they occupy on each side.\n * The keyboard navigation uses this to ensure focused cells are fully visible.\n *\n * @param rowEl - The row element (optional, for calculating widths from rendered cells)\n * @param focusedCell - The currently focused cell element (optional, to determine if scrolling should be skipped)\n * @returns Object with left/right pixel offsets and optional skipScroll flag, or undefined if plugin has no offsets\n *\n * @example\n * ```ts\n * getHorizontalScrollOffsets(rowEl?: HTMLElement, focusedCell?: HTMLElement): { left: number; right: number; skipScroll?: boolean } | undefined {\n * // Calculate total width of left-pinned columns\n * const leftCells = rowEl?.querySelectorAll('.sticky-left') ?? [];\n * let left = 0;\n * leftCells.forEach(el => { left += (el as HTMLElement).offsetWidth; });\n * // Skip scroll if focused cell is pinned (always visible)\n * const skipScroll = focusedCell?.classList.contains('sticky-left');\n * return { left, right: 0, skipScroll };\n * }\n * ```\n */\n getHorizontalScrollOffsets?(\n rowEl?: HTMLElement,\n focusedCell?: HTMLElement,\n ): { left: number; right: number; skipScroll?: boolean } | undefined;\n\n // #endregion\n\n // #region Shell Integration Hooks\n\n /**\n * Register a tool panel for this plugin.\n * Return undefined if plugin has no tool panel.\n * The shell will create a toolbar toggle button and render the panel content\n * when the user opens the panel.\n *\n * @returns Tool panel definition, or undefined if plugin has no panel\n *\n * @example\n * ```ts\n * getToolPanel(): ToolPanelDefinition | undefined {\n * return {\n * id: 'columns',\n * title: 'Columns',\n * icon: '☰',\n * tooltip: 'Show/hide columns',\n * order: 10,\n * render: (container) => {\n * this.renderColumnList(container);\n * return () => this.cleanup();\n * },\n * };\n * }\n * ```\n */\n getToolPanel?(): ToolPanelDefinition | undefined;\n\n /**\n * Register content for the shell header center section.\n * Return undefined if plugin has no header content.\n * Examples: search input, selection summary, status indicators.\n *\n * @returns Header content definition, or undefined if plugin has no header content\n *\n * @example\n * ```ts\n * getHeaderContent(): HeaderContentDefinition | undefined {\n * return {\n * id: 'quick-filter',\n * order: 10,\n * render: (container) => {\n * const input = document.createElement('input');\n * input.type = 'text';\n * input.placeholder = 'Search...';\n * input.addEventListener('input', this.handleInput);\n * container.appendChild(input);\n * return () => input.removeEventListener('input', this.handleInput);\n * },\n * };\n * }\n * ```\n */\n getHeaderContent?(): HeaderContentDefinition | undefined;\n\n // #endregion\n}\n","/**\n * 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// #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 // ─── Shared Attributes (used by plugins) ──────────────────────────\n GROUP_KEY: 'data-group-key', // GroupingRowsPlugin\n} as const;\n\n// #endregion\n\n// #region Selectors\n\n/**\n * Common CSS selectors for querying grid elements.\n * Built from the class constants for consistency.\n *\n * @category Plugin Development\n * @since 0.1.1\n */\nexport const GridSelectors = {\n ROOT: `.${GridClasses.ROOT}`,\n HEADER: `.${GridClasses.HEADER}`,\n HEADER_ROW: `.${GridClasses.HEADER_ROW}`,\n HEADER_CELL: `.${GridClasses.HEADER_CELL}`,\n ROWS_VIEWPORT: `.${GridClasses.ROWS_VIEWPORT}`,\n ROWS_CONTAINER: `.${GridClasses.ROWS_CONTAINER}`,\n DATA_ROW: `.${GridClasses.DATA_ROW}`,\n DATA_CELL: `.${GridClasses.DATA_CELL}`,\n GROUP_ROW: `.${GridClasses.GROUP_ROW}`,\n\n // By data attribute\n CELL_BY_FIELD: (field: string) => `.${GridClasses.DATA_CELL}[${GridDataAttrs.FIELD}=\"${field}\"]`,\n\n // State selectors\n SELECTED_ROWS: `.${GridClasses.DATA_ROW}.${GridClasses.SELECTED}`,\n EDITING_CELL: `.${GridClasses.DATA_CELL}.${GridClasses.EDITING}`,\n} as const;\n\n// #endregion\n\n// #region CSS Custom Properties\n\n/**\n * CSS custom property names for theming.\n * Use these when programmatically setting styles.\n *\n * @category Plugin Development\n * @since 0.1.1\n */\nexport const GridCSSVars = {\n // Colors\n COLOR_BG: '--tbw-color-bg',\n COLOR_FG: '--tbw-color-fg',\n COLOR_FG_MUTED: '--tbw-color-fg-muted',\n COLOR_BORDER: '--tbw-color-border',\n COLOR_ACCENT: '--tbw-color-accent',\n COLOR_HEADER_BG: '--tbw-color-header-bg',\n COLOR_HEADER_FG: '--tbw-color-header-fg',\n COLOR_SELECTION: '--tbw-color-selection',\n COLOR_ROW_HOVER: '--tbw-color-row-hover',\n COLOR_ROW_ALT: '--tbw-color-row-alt',\n\n // Sizing\n ROW_HEIGHT: '--tbw-row-height',\n HEADER_HEIGHT: '--tbw-header-height',\n CELL_PADDING: '--tbw-cell-padding',\n\n // Typography\n FONT_FAMILY: '--tbw-font-family',\n FONT_SIZE: '--tbw-font-size',\n\n // Borders\n BORDER_RADIUS: '--tbw-border-radius',\n FOCUS_OUTLINE: '--tbw-focus-outline',\n} as const;\n\n// #endregion\n\n// Type helpers\n\n/**\n * Union of all CSS class names used in the grid's DOM.\n * Use `GridClasses` to access individual values by key.\n *\n * @category Plugin Development\n * @since 0.1.1\n */\nexport type GridClassName = (typeof GridClasses)[keyof typeof GridClasses];\n\n/**\n * Union of all `data-*` attribute names used on grid elements.\n * Use `GridDataAttrs` to access individual values by key.\n *\n * @category Plugin Development\n * @since 0.1.1\n */\nexport type GridDataAttr = (typeof GridDataAttrs)[keyof typeof GridDataAttrs];\n\n/**\n * Union of all CSS custom property names (`--tbw-*`) available for theming.\n * Use `GridCSSVars` to access individual values by key.\n *\n * @category Plugin Development\n * @since 0.1.1\n */\nexport type GridCSSVar = (typeof GridCSSVars)[keyof typeof GridCSSVars];\n","/**\n * DOM Builder - Direct DOM construction for performance.\n *\n * Using direct DOM APIs instead of innerHTML is significantly faster:\n * - No HTML parsing by the browser\n * - No template string concatenation\n * - Immediate element creation without serialization/deserialization\n *\n * Benchmark: DOM construction is ~2-3x faster than innerHTML for complex structures.\n */\n\nimport { GridClasses } from '../constants';\nimport { sanitizeHTML } from './sanitize';\n\n// #region Element Factories\n/**\n * Apply attributes to an element, skipping null/undefined values.\n * Shared by createElement/div/button to keep the bundle small.\n */\nfunction applyAttrs(el: Element, attrs: Record<string, string | undefined> | undefined): void {\n if (!attrs) return;\n for (const key in attrs) {\n const value = attrs[key];\n if (value != null) el.setAttribute(key, value);\n }\n}\n\n/**\n * Create an element with attributes and optional children.\n * Optimized helper that avoids repeated function calls.\n */\nfunction createElement<K extends keyof HTMLElementTagNameMap>(\n tag: K,\n attrs?: Record<string, string>,\n children?: (Node | string | null | undefined)[],\n): HTMLElementTagNameMap[K] {\n const el = document.createElement(tag);\n applyAttrs(el, attrs);\n\n if (children) {\n for (const child of children) {\n if (child == null) continue;\n if (typeof child === 'string') {\n el.appendChild(document.createTextNode(child));\n } else {\n el.appendChild(child);\n }\n }\n }\n\n return el;\n}\n\n/**\n * Create an element with class (common pattern).\n */\nfunction div(className?: string, attrs?: Record<string, string>): HTMLDivElement {\n const el = document.createElement('div');\n if (className) el.className = className;\n applyAttrs(el, attrs);\n return el;\n}\n\n/**\n * Create a button element. Always sets `type=\"button\"` (callers can override\n * via `attrs`) so internal grid buttons inside a <form> never default to\n * type=\"submit\". See issue #296.\n */\nfunction button(className?: string, attrs?: Record<string, string>, content?: string | Node): HTMLButtonElement {\n const el = document.createElement('button');\n el.type = 'button';\n if (className) el.className = className;\n applyAttrs(el, attrs);\n if (content) {\n if (typeof content === 'string') {\n el.textContent = content;\n } else {\n el.appendChild(content);\n }\n }\n return el;\n}\n// #endregion\n\n// #region Grid Templates\n/**\n * Template for grid content (the core scrollable grid area).\n * Pre-built once, then cloned for each grid instance.\n */\nconst gridContentTemplate = document.createElement('template');\ngridContentTemplate.innerHTML = `\n <div class=\"tbw-scroll-area\">\n <div class=\"rows-body-wrapper\">\n <div class=\"rows-body\" role=\"grid\">\n <div class=\"header\" role=\"rowgroup\">\n <div class=\"header-row\" role=\"row\" part=\"header-row\"></div>\n </div>\n <div class=\"rows-container\" role=\"presentation\">\n <div class=\"rows-viewport\" role=\"presentation\">\n <div class=\"rows\"></div>\n </div>\n </div>\n </div>\n </div>\n </div>\n <div class=\"faux-vscroll\">\n <div class=\"faux-vscroll-spacer\"></div>\n </div>\n <div class=\"tbw-sr-only\" aria-live=\"polite\" aria-atomic=\"true\"></div>\n`;\n\n/**\n * Clone the grid content structure.\n * Using template cloning is faster than createElement for nested structures.\n */\nexport function cloneGridContent(): DocumentFragment {\n return gridContentTemplate.content.cloneNode(true) as DocumentFragment;\n}\n// #endregion\n\n// #region Grid DOM Building\n/**\n * Build the grid root structure using direct DOM construction.\n * This is called once per grid instance during initial render.\n */\nexport interface GridDOMOptions {\n hasShell: boolean;\n /** Shell header element (pre-built) */\n shellHeader?: Element;\n /** Shell body element with tool panel (pre-built) */\n shellBody?: Element;\n}\n\n/**\n * Build the complete grid DOM structure.\n * Returns a DocumentFragment ready to be appended to shadow root.\n */\nexport function buildGridDOM(options: GridDOMOptions): DocumentFragment {\n const fragment = document.createDocumentFragment();\n\n const root = div(options.hasShell ? `${GridClasses.ROOT} has-shell` : GridClasses.ROOT);\n\n if (options.hasShell && options.shellBody) {\n // Shell mode: optional header + body (with grid content inside).\n // The header bar may be suppressed via `shell.header.visible: false`, in\n // which case only the body (and its tool panel) is rendered.\n if (options.shellHeader) root.appendChild(options.shellHeader);\n root.appendChild(options.shellBody);\n } else {\n // No shell: just grid content in wrapper\n const contentWrapper = div('tbw-grid-content');\n contentWrapper.appendChild(cloneGridContent());\n root.appendChild(contentWrapper);\n }\n\n fragment.appendChild(root);\n return fragment;\n}\n\n/**\n * Build the BARE grid DOM (no shell chrome) directly into `renderRoot`.\n *\n * Preserves all existing direct child nodes EXCEPT the grid's own\n * `.tbw-grid-root` (which is rebuilt), so user/framework light-DOM stays intact\n * in its original order — including non-element nodes (comment anchors used by\n * Angular `@if`/`@for` and other structural directives as positional markers).\n *\n * Core's render path always builds the bare grid; the shell, when configured,\n * is wrapped around this freshly built grid by the auto-registered ShellPlugin\n * in the `afterStructuralRender` hook (extraction #370 — DOM ownership\n * inversion). Core therefore holds no shell-construction knowledge.\n */\nexport function buildBareGridDOMIntoElement(renderRoot: Element): void {\n const preservedNodes: Node[] = [];\n for (let n: Node | null = renderRoot.firstChild; n; n = n.nextSibling) {\n if (n.nodeType === 1 && (n as Element).classList.contains(GridClasses.ROOT)) continue;\n preservedNodes.push(n);\n }\n\n // Clear existing content (this would delete light DOM nodes, so we preserved them first).\n renderRoot.replaceChildren();\n\n // Re-append preserved nodes in original order (hidden elements are used for config).\n // IMPORTANT: These are prepended before .tbw-grid-root, so `renderRoot.children[0]`\n // is NOT the grid root. Use `querySelector('.tbw-grid-root')` instead.\n for (const n of preservedNodes) {\n renderRoot.appendChild(n);\n }\n\n renderRoot.appendChild(buildGridDOM({ hasShell: false }));\n}\n// #endregion\n\n// #region Shell Header\n/**\n * Build shell header using direct DOM construction.\n *\n * Note: The grid no longer creates buttons from config (icon/action).\n * Config/API buttons only use element or render function.\n * Light DOM buttons are slotted directly.\n * The ONLY button the grid creates is the panel toggle.\n */\nexport interface ShellHeaderOptions {\n title?: string;\n hasPanels: boolean;\n isPanelOpen: boolean;\n toolPanelIcon?: string;\n /**\n * Whether to render the built-in tool panel toggle button and the\n * adjacent separator. Independent of `hasPanels` so panels can remain\n * registered (and openable via the public API) while the consumer\n * supplies their own toggle button.\n * @default true when hasPanels is true\n */\n showToggle?: boolean;\n /** Config toolbar contents with render function (pre-sorted by order) */\n configButtons: Array<{\n id: string;\n hasRender?: boolean;\n }>;\n /** API toolbar contents with render function (pre-sorted by order) */\n apiButtons: Array<{\n id: string;\n hasRender?: boolean;\n }>;\n}\n\n/**\n * Build shell header element directly without innerHTML.\n */\nexport function buildShellHeader(options: ShellHeaderOptions): HTMLDivElement {\n const header = div('tbw-shell-header', { part: 'shell-header', role: 'presentation' });\n\n // Title\n if (options.title) {\n const titleEl = div('tbw-shell-title');\n titleEl.textContent = options.title;\n header.appendChild(titleEl);\n }\n\n // Shell content with placeholder for light DOM header content\n const content = div('tbw-shell-content', {\n part: 'shell-content',\n role: 'presentation',\n 'data-light-dom-header-content': '',\n });\n header.appendChild(content);\n\n // Toolbar\n const toolbar = div('tbw-shell-toolbar', { part: 'shell-toolbar', role: 'presentation' });\n\n // Placeholders for config toolbar contents with render function\n for (const btn of options.configButtons) {\n if (btn.hasRender) {\n toolbar.appendChild(div('tbw-toolbar-content-slot', { 'data-toolbar-content': btn.id }));\n }\n }\n // Placeholders for API toolbar contents with render function\n for (const btn of options.apiButtons) {\n if (btn.hasRender) {\n toolbar.appendChild(div('tbw-toolbar-content-slot', { 'data-toolbar-content': btn.id }));\n }\n }\n\n // Separator between custom content and panel toggle\n const hasCustomContent =\n options.configButtons.some((b) => b.hasRender) || options.apiButtons.some((b) => b.hasRender);\n // Default showToggle to hasPanels for backwards compat; suppress both the\n // button and the separator when the consumer opts out.\n const showToggle = options.hasPanels && options.showToggle !== false;\n if (hasCustomContent && showToggle) {\n toolbar.appendChild(div('tbw-toolbar-separator'));\n }\n\n // Panel toggle button (suppressed when shell.header.toolPanelToggle is false)\n if (showToggle) {\n const toggleBtn = button(options.isPanelOpen ? 'tbw-toolbar-btn active' : 'tbw-toolbar-btn', {\n 'data-panel-toggle': '',\n title: 'Settings',\n 'aria-label': 'Toggle settings panel',\n 'aria-pressed': String(options.isPanelOpen),\n 'aria-controls': 'tbw-tool-panel',\n });\n toggleBtn.dataset.icon = 'tool-panel';\n if (options.toolPanelIcon !== undefined) {\n toggleBtn.innerHTML = sanitizeHTML(options.toolPanelIcon);\n }\n toolbar.appendChild(toggleBtn);\n }\n\n header.appendChild(toolbar);\n return header;\n}\n// #endregion\n\n// #region Shell Body\n/**\n * Build shell body (grid content + optional tool panel).\n */\nexport interface ShellBodyOptions {\n position: 'left' | 'right';\n /**\n * Layout mode for the tool panel.\n * - `'overlay'` (default): panel is absolutely positioned over the grid.\n * - `'push'`: panel participates in flex layout, shrinking the grid sibling.\n * - `'dropdown'`: panel is a native popover anchored near a trigger element.\n */\n mode?: 'overlay' | 'push' | 'dropdown';\n isPanelOpen: boolean;\n expandIcon?: string;\n collapseIcon?: string;\n /**\n * Render an in-panel close (✕) button in the panel's top-right corner.\n * Used when the shell header bar is hidden (`shell.header.visible: false`)\n * so the panel can be dismissed without the built-in header toggle.\n */\n showCloseButton?: boolean;\n /** Sorted panels for accordion */\n panels: Array<{\n id: string;\n title: string;\n icon?: string;\n isExpanded: boolean;\n }>;\n}\n\n/**\n * Build shell body element directly without innerHTML.\n */\nexport function buildShellBody(options: ShellBodyOptions): HTMLDivElement {\n const mode = options.mode ?? 'overlay';\n const body = div('tbw-shell-body', { 'data-mode': mode });\n const hasPanel = options.panels.length > 0;\n const isSinglePanel = options.panels.length === 1;\n\n // Grid content wrapper with cloned grid structure\n const gridContent = div('tbw-grid-content');\n gridContent.appendChild(cloneGridContent());\n\n // Tool panel\n let panelEl: HTMLElement | null = null;\n if (hasPanel) {\n panelEl = createElement('aside', {\n class: options.isPanelOpen ? 'tbw-tool-panel open' : 'tbw-tool-panel',\n part: 'tool-panel',\n 'data-position': options.position,\n role: 'presentation',\n id: 'tbw-tool-panel',\n // In dropdown mode the panel is a manually-controlled native popover\n // (shown/hidden via showPopover()/hidePopover() from the controller).\n ...(mode === 'dropdown' ? { popover: 'manual' } : {}),\n });\n\n // Resize handle\n const resizeHandlePosition = options.position === 'left' ? 'right' : 'left';\n panelEl.appendChild(\n div('tbw-tool-panel-resize', {\n 'data-resize-handle': '',\n 'data-handle-position': resizeHandlePosition,\n 'aria-hidden': 'true',\n }),\n );\n\n // Optional close (✕) button — rendered (overlay/push only) when the shell\n // header bar is hidden, so the panel can always be dismissed. Dropdown mode\n // never reaches here (showCloseButton is gated off upstream). `data-panel-close`\n // is wired by setupShellEventListeners to close the panel.\n //\n // Placement:\n // - Multiple panels: a dedicated slim header row above the accordion.\n // - Single panel: inlined onto the (only) accordion header row so it shares\n // the line with the panel title instead of wasting a full-width row.\n const makeCloseButton = (inline: boolean): HTMLButtonElement => {\n const closeBtn = button(inline ? 'tbw-tool-panel-close tbw-tool-panel-close--inline' : 'tbw-tool-panel-close', {\n part: 'tool-panel-close',\n 'data-panel-close': '',\n title: 'Close panel',\n 'aria-label': 'Close panel',\n 'aria-controls': 'tbw-tool-panel',\n });\n closeBtn.textContent = '✕';\n return closeBtn;\n };\n const inlineClose = options.showCloseButton === true && isSinglePanel;\n if (options.showCloseButton && !isSinglePanel) {\n const panelHeader = div('tbw-tool-panel-header', { part: 'tool-panel-header' });\n panelHeader.appendChild(makeCloseButton(false));\n panelEl.appendChild(panelHeader);\n }\n\n // Panel content with accordion\n const panelContent = div('tbw-tool-panel-content', { role: 'presentation' });\n const accordion = div('tbw-accordion');\n\n for (const panel of options.panels) {\n const sectionClasses = `tbw-accordion-section${panel.isExpanded ? ' expanded' : ''}${isSinglePanel ? ' single' : ''}${inlineClose ? ' has-inline-close' : ''}`;\n const section = div(sectionClasses, { 'data-section': panel.id });\n\n // Accordion header button\n const headerBtn = button('tbw-accordion-header', {\n 'aria-expanded': String(panel.isExpanded),\n 'aria-controls': `tbw-section-${panel.id}`,\n });\n if (isSinglePanel) headerBtn.setAttribute('aria-disabled', 'true');\n\n // Icon\n if (panel.icon) {\n const iconSpan = createElement('span', { class: 'tbw-accordion-icon' });\n iconSpan.innerHTML = sanitizeHTML(panel.icon);\n headerBtn.appendChild(iconSpan);\n }\n\n // Title\n const titleSpan = createElement('span', { class: 'tbw-accordion-title' });\n titleSpan.textContent = panel.title;\n headerBtn.appendChild(titleSpan);\n\n // Chevron (hidden for single panel) — always use expandIcon, CSS rotation handles state\n if (!isSinglePanel) {\n const chevronSpan = createElement('span', { class: 'tbw-accordion-chevron', 'data-icon': 'expand' });\n if (options.expandIcon !== undefined) {\n chevronSpan.innerHTML = sanitizeHTML(options.expandIcon);\n }\n headerBtn.appendChild(chevronSpan);\n }\n\n // Single-panel close button shares the header row with the panel title.\n if (inlineClose) {\n const headerRow = div('tbw-accordion-header-row');\n headerRow.appendChild(headerBtn);\n headerRow.appendChild(makeCloseButton(true));\n section.appendChild(headerRow);\n } else {\n section.appendChild(headerBtn);\n }\n\n // Accordion content (empty, will be filled by panel render functions)\n section.appendChild(\n div('tbw-accordion-content', {\n id: `tbw-section-${panel.id}`,\n role: 'presentation',\n }),\n );\n\n accordion.appendChild(section);\n }\n\n panelContent.appendChild(accordion);\n panelEl.appendChild(panelContent);\n }\n\n // Append in correct order based on position\n if (options.position === 'left' && panelEl) {\n body.appendChild(panelEl);\n body.appendChild(gridContent);\n } else {\n body.appendChild(gridContent);\n if (panelEl) body.appendChild(panelEl);\n }\n\n return body;\n}\n// #endregion\n","/**\n * Shell infrastructure for grid header bar and tool panels.\n *\n * The shell is an optional wrapper layer that provides:\n * - Header bar with title, plugin content, and toolbar buttons\n * - Tool panels that plugins can register content into\n * - Light DOM parsing for framework-friendly configuration\n */\n\nimport { TOOL_PANEL_DUPLICATE, TOOL_PANEL_MISSING_ATTR, warnDiagnostic } from '../../core/internal/diagnostics';\nimport { escapeHtml, sanitizeHTML } from '../../core/internal/sanitize';\nimport type { IconValue } from '../../core/types';\nimport type { HeaderContentDefinition, ShellConfig, ToolbarContentDefinition, ToolPanelDefinition } from './types';\n\n// #region Types & State\n/**\n * Convert an IconValue to a string for rendering in HTML.\n */\nfunction iconToString(icon: IconValue | undefined): string {\n if (!icon) return '';\n if (typeof icon === 'string') return sanitizeHTML(icon);\n // For HTMLElement, get the outerHTML\n return icon.outerHTML;\n}\n\n/**\n * State for managing shell UI.\n *\n * This interface holds both configuration-like properties (toolPanels, headerContents)\n * and runtime state (isPanelOpen, expandedSections). The Maps allow for efficient\n * registration/unregistration of panels and content.\n */\nexport interface ShellState {\n /** Registered tool panels (from plugins + consumer API) */\n toolPanels: Map<string, ToolPanelDefinition>;\n /** Registered header content (from plugins + consumer API) */\n headerContents: Map<string, HeaderContentDefinition>;\n /** Toolbar content registered via API or light DOM */\n toolbarContents: Map<string, ToolbarContentDefinition>;\n /** Whether a <tbw-grid-tool-buttons> container was found in light DOM */\n hasToolButtonsContainer: boolean;\n /** Light DOM header content elements */\n lightDomHeaderContent: HTMLElement[];\n /** Light DOM header title from <tbw-grid-header title=\"...\"> */\n lightDomTitle: string | null;\n /** IDs of tool panels registered from light DOM (to avoid re-parsing) */\n lightDomToolPanelIds: Set<string>;\n /**\n * IDs of light DOM tool panels whose `render` has already been bound to a\n * framework-adapter renderer. Used by `parseLightDomToolPanels` to detect\n * the FIRST adapter attach (which legitimately needs to swap the vanilla\n * fallback for the real adapter renderer and tear down any in-progress\n * render) vs. subsequent re-parses (which must NOT tear down — doing so\n * unmounts the user's React/Vue/Angular component, losing local state and\n * resetting scroll position inside the panel).\n */\n adapterBoundToolPanelIds: Set<string>;\n /** IDs of toolbar content registered from light DOM (to avoid re-parsing) */\n lightDomToolbarContentIds: Set<string>;\n /** IDs of tool panels registered via registerToolPanel API */\n apiToolPanelIds: Set<string>;\n /** IDs of header content registered via registerHeaderContent API */\n apiHeaderContentIds: Set<string>;\n /** Whether the tool panel sidebar is open */\n isPanelOpen: boolean;\n /** Which accordion sections are expanded (by panel ID) */\n expandedSections: Set<string>;\n /** Whether light DOM header content has been moved to placeholder (perf optimization) */\n lightDomContentMoved: boolean;\n /** Cleanup functions for header content render returns */\n headerContentCleanups: Map<string, () => void>;\n /** Cleanup functions for each panel section's render return */\n panelCleanups: Map<string, () => void>;\n /** Cleanup functions for toolbar content render returns */\n toolbarContentCleanups: Map<string, () => void>;\n /**\n * Element the dropdown popover is currently anchored to (mode: 'dropdown').\n * Tracked so dismissal logic can ignore clicks on the anchor and the\n * `anchor-name` it set can be cleared on close. `null` when no dropdown is open.\n */\n dropdownAnchorEl?: HTMLElement | null;\n /**\n * Unique `anchor-name` assigned to the current dropdown's trigger (and\n * mirrored as `position-anchor` on the popover). Minted per show so multiple\n * grids on one page never collide on a shared anchor name — a collision would\n * position one grid's popover against another grid's trigger. `null` when no\n * dropdown is open.\n */\n dropdownAnchorName?: string | null;\n}\n\n/**\n * Runtime-only shell state (not configuration).\n *\n * Configuration (toolPanels, headerContents, toolbarContents, title) lives in\n * effectiveConfig.shell. This state holds runtime UI state and cleanup functions.\n */\nexport interface ShellRuntimeState {\n /** Whether the tool panel sidebar is currently open */\n isPanelOpen: boolean;\n /** Which accordion sections are currently expanded (by panel ID) */\n expandedSections: Set<string>;\n /** Cleanup functions for header content render returns */\n headerContentCleanups: Map<string, () => void>;\n /** Cleanup functions for each panel section's render return */\n panelCleanups: Map<string, () => void>;\n /** Cleanup functions for toolbar content render returns */\n toolbarContentCleanups: Map<string, () => void>;\n /** IDs of tool panels registered from light DOM (to avoid re-parsing) */\n lightDomToolPanelIds: Set<string>;\n /** IDs of tool panels registered via registerToolPanel API */\n apiToolPanelIds: Set<string>;\n /** Whether a <tbw-grid-tool-buttons> container was found in light DOM */\n hasToolButtonsContainer: boolean;\n}\n\n/**\n * Create initial shell runtime state.\n */\nexport function createShellRuntimeState(): ShellRuntimeState {\n return {\n isPanelOpen: false,\n expandedSections: new Set(),\n headerContentCleanups: new Map(),\n panelCleanups: new Map(),\n toolbarContentCleanups: new Map(),\n lightDomToolPanelIds: new Set(),\n apiToolPanelIds: new Set(),\n hasToolButtonsContainer: false,\n };\n}\n\n/**\n * Create initial shell state.\n */\nexport function createShellState(): ShellState {\n return {\n toolPanels: new Map(),\n headerContents: new Map(),\n toolbarContents: new Map(),\n hasToolButtonsContainer: false,\n lightDomHeaderContent: [],\n lightDomTitle: null,\n lightDomToolPanelIds: new Set(),\n adapterBoundToolPanelIds: new Set(),\n lightDomToolbarContentIds: new Set(),\n apiToolPanelIds: new Set(),\n apiHeaderContentIds: new Set(),\n isPanelOpen: false,\n expandedSections: new Set(),\n headerContentCleanups: new Map(),\n panelCleanups: new Map(),\n toolbarContentCleanups: new Map(),\n lightDomContentMoved: false,\n dropdownAnchorEl: null,\n dropdownAnchorName: null,\n };\n}\n// #endregion\n\n// #region Render Functions\n/**\n * Determine if shell header should be rendered.\n * Reads only from effectiveConfig.shell (single source of truth).\n */\nexport function shouldRenderShellHeader(config: ShellConfig | undefined): boolean {\n // Check if title is configured\n if (config?.header?.title) return true;\n\n // Check if config has toolbar contents\n if (config?.header?.toolbarContents?.length) return true;\n\n // Check if any tool panels are registered\n if (config?.toolPanels?.length) return true;\n\n // Check if any header content is registered\n if (config?.headerContents?.length) return true;\n\n // Check if light DOM has header elements\n if (config?.header?.lightDomContent?.length) return true;\n\n // Check if a toolbar buttons container was found\n if (config?.header?.hasToolButtonsContainer) return true;\n\n return false;\n}\n\n/**\n * Determine whether the visible shell header bar (`.tbw-shell-header`) should\n * be rendered.\n *\n * Distinct from {@link shouldRenderShellHeader}, which decides whether the\n * shell *wrapper* (including the body that hosts the tool panel) exists at\n * all. The header bar can be suppressed via `header.visible: false` while the\n * shell body and tool panels still render — letting consumers drive panels\n * from their own UI (e.g. a utility-column header icon).\n */\nexport function shouldRenderHeaderBar(config: ShellConfig | undefined): boolean {\n return config?.header?.visible !== false;\n}\n\n/**\n * Render the shell header HTML.\n *\n * Toolbar contents come from two sources:\n * 1. Light DOM slot (users provide their own HTML in <tbw-grid-tool-buttons>)\n * 2. Config/API with render function (programmatic insertion)\n *\n * Users have full control over toolbar HTML, styling, and behavior.\n * The only button the grid creates is the tool panel toggle.\n *\n * @param toolPanelIcon - Icon for the tool panel toggle (from grid icon config)\n */\nexport function renderShellHeader(\n config: ShellConfig | undefined,\n state: ShellState,\n toolPanelIcon?: IconValue,\n): string {\n const title = config?.header?.title ?? state.lightDomTitle ?? '';\n const hasTitle = !!title;\n\n // Build tool panel button content: use data-icon for CSS, inject content only for JS overrides\n let toolPanelBtnContent = '';\n if (toolPanelIcon !== undefined) {\n // JS override: inject content but still set data-icon\n toolPanelBtnContent = typeof toolPanelIcon === 'string' ? sanitizeHTML(toolPanelIcon) : toolPanelIcon.outerHTML;\n }\n\n // Get all toolbar contents from effectiveConfig (already merged: config + API + light DOM)\n // The config-manager merges state.toolbarContents into effectiveConfig.shell.header.toolbarContents\n // Also include state.toolbarContents directly for cases where renderShellHeader is called\n // before config-manager has merged (e.g., unit tests, initial render)\n const configContents = config?.header?.toolbarContents ?? [];\n const stateContents = [...state.toolbarContents.values()];\n\n // Merge: use config contents, add state contents that aren't in config\n const configIds = new Set(configContents.map((c) => c.id));\n const allContents = [...configContents];\n for (const content of stateContents) {\n if (!configIds.has(content.id)) {\n allContents.push(content);\n }\n }\n\n const hasCustomContent = allContents.length > 0;\n const hasPanels = state.toolPanels.size > 0;\n // Allow consumers to suppress the built-in toggle (and the separator that\n // only exists to space it from custom toolbar contents) so they can render\n // their own design-system button and wire it to `grid.toggleToolPanel()`.\n // Also suppress when the tool panel is locked open — toggling is disabled.\n // Default true preserves existing behavior.\n const isLocked = config?.toolPanel?.locked === true;\n const showToggle = hasPanels && config?.header?.toolPanelToggle !== false && !isLocked;\n const showSeparator = hasCustomContent && showToggle;\n\n // Sort contents by order for slot placement\n const sortedContents = [...allContents].sort((a, b) => (a.order ?? 0) - (b.order ?? 0));\n\n // Build toolbar HTML\n let toolbarHtml = '';\n\n // Create slots for all contents (unified: config + API + light DOM)\n for (const content of sortedContents) {\n toolbarHtml += `<div class=\"tbw-toolbar-content-slot\" data-toolbar-content=\"${content.id}\"></div>`;\n }\n\n // Separator between custom content and panel toggle\n if (showSeparator) {\n toolbarHtml += '<div class=\"tbw-toolbar-separator\"></div>';\n }\n\n // Single panel toggle button (the ONLY button the grid creates).\n // Suppressed when `shell.header.toolPanelToggle === false` so consumers can\n // BYO toggle button without hiding library DOM via CSS.\n if (showToggle) {\n const isOpen = state.isPanelOpen;\n const toggleClass = isOpen ? 'tbw-toolbar-btn active' : 'tbw-toolbar-btn';\n // type=\"button\" is required: without it a <button> inside a <form>\n // defaults to type=\"submit\" and clicking the toggle would submit the\n // surrounding form. Internal grid buttons must never trigger form\n // submission. See issue #296.\n toolbarHtml += `<button type=\"button\" class=\"${toggleClass}\" data-panel-toggle data-icon=\"tool-panel\" title=\"Settings\" aria-label=\"Toggle settings panel\" aria-pressed=\"${isOpen}\" aria-controls=\"tbw-tool-panel\">${toolPanelBtnContent}</button>`;\n }\n\n return `\n <div class=\"tbw-shell-header\" part=\"shell-header\" role=\"presentation\">\n ${hasTitle ? `<div class=\"tbw-shell-title\">${escapeHtml(title)}</div>` : ''}\n <div class=\"tbw-shell-content\" part=\"shell-content\" role=\"presentation\" data-light-dom-header-content></div>\n <div class=\"tbw-shell-toolbar\" part=\"shell-toolbar\" role=\"presentation\">\n ${toolbarHtml}\n </div>\n </div>\n `;\n}\n\n/**\n * Render the shell body wrapper HTML (contains grid content + accordion-style tool panel).\n * @param icons - Optional icons for expand/collapse chevrons (from grid config)\n */\nexport function renderShellBody(\n config: ShellConfig | undefined,\n state: ShellState,\n gridContentHtml: string,\n icons?: { expand?: IconValue; collapse?: IconValue },\n): string {\n const position = config?.toolPanel?.position ?? 'right';\n const mode = config?.toolPanel?.mode ?? 'overlay';\n const hasPanel = state.toolPanels.size > 0;\n const isOpen = state.isPanelOpen;\n const hasJsExpandIcon = icons?.expand !== undefined;\n const expandIcon = hasJsExpandIcon ? iconToString(icons.expand) : '';\n\n // Sort panels by order for accordion sections\n const sortedPanels = [...state.toolPanels.values()].sort((a, b) => (a.order ?? 100) - (b.order ?? 100));\n const isSinglePanel = sortedPanels.length === 1;\n\n // Build accordion sections HTML\n let accordionHtml = '';\n for (const panel of sortedPanels) {\n const isExpanded = state.expandedSections.has(panel.id);\n const iconHtml = panel.icon ? `<span class=\"tbw-accordion-icon\">${panel.icon}</span>` : '';\n // Hide chevron for single panel (no toggling needed)\n // Always use expandIcon (▶) — CSS rotation handles the expanded state\n const chevronHtml = isSinglePanel\n ? ''\n : `<span class=\"tbw-accordion-chevron\" data-icon=\"expand\">${expandIcon}</span>`;\n // Disable accordion toggle for single panel\n const sectionClasses = `tbw-accordion-section${isExpanded ? ' expanded' : ''}${isSinglePanel ? ' single' : ''}`;\n accordionHtml += `\n <div class=\"${sectionClasses}\" data-section=\"${panel.id}\">\n <button type=\"button\" class=\"tbw-accordion-header\" aria-expanded=\"${isExpanded}\" aria-controls=\"tbw-section-${panel.id}\"${isSinglePanel ? ' aria-disabled=\"true\"' : ''}>\n ${iconHtml}\n <span class=\"tbw-accordion-title\">${panel.title}</span>\n ${chevronHtml}\n </button>\n <div class=\"tbw-accordion-content\" id=\"tbw-section-${panel.id}\" role=\"presentation\"></div>\n </div>\n `;\n }\n\n // Resize handle position depends on panel position\n const resizeHandlePosition = position === 'left' ? 'right' : 'left';\n\n const panelHtml = hasPanel\n ? `\n <aside class=\"tbw-tool-panel${isOpen ? ' open' : ''}\" part=\"tool-panel\" data-position=\"${position}\" role=\"presentation\" id=\"tbw-tool-panel\"${mode === 'dropdown' ? ' popover=\"manual\"' : ''}>\n <div class=\"tbw-tool-panel-resize\" data-resize-handle data-handle-position=\"${resizeHandlePosition}\" aria-hidden=\"true\"></div>\n <div class=\"tbw-tool-panel-content\" role=\"presentation\">\n <div class=\"tbw-accordion\">\n ${accordionHtml}\n </div>\n </div>\n </aside>\n `\n : '';\n\n // For left position, panel comes before content in DOM order\n if (position === 'left') {\n return `\n <div class=\"tbw-shell-body\" data-mode=\"${mode}\">\n ${panelHtml}\n <div class=\"tbw-grid-content\">\n ${gridContentHtml}\n </div>\n </div>\n `;\n }\n\n return `\n <div class=\"tbw-shell-body\" data-mode=\"${mode}\">\n <div class=\"tbw-grid-content\">\n ${gridContentHtml}\n </div>\n ${panelHtml}\n </div>\n `;\n}\n// #endregion\n\n// #region Light DOM Parsing\n/**\n * Parse light DOM shell elements (tbw-grid-header, etc.).\n * Safe to call multiple times - will only parse once when elements are available.\n */\nexport function parseLightDomShell(host: HTMLElement, state: ShellState): void {\n const headerEl = host.querySelector('tbw-grid-header');\n if (!headerEl) return;\n\n // Parse title attribute (only if not already parsed)\n if (!state.lightDomTitle) {\n const title = headerEl.getAttribute('title');\n if (title) {\n state.lightDomTitle = title;\n }\n }\n\n // Parse header content elements - store references but don't set slot (light DOM doesn't use slots)\n const headerContents = headerEl.querySelectorAll('tbw-grid-header-content');\n if (headerContents.length > 0 && state.lightDomHeaderContent.length === 0) {\n state.lightDomHeaderContent = Array.from(headerContents) as HTMLElement[];\n }\n\n // Hide the light DOM header container (it was just for declarative config)\n (headerEl as HTMLElement).style.display = 'none';\n}\n\n/**\n * Callback type for creating a toolbar content renderer from a light DOM container.\n * This is used by framework adapters (Angular, React, etc.) to create renderers\n * from their template syntax.\n */\nexport type ToolbarContentRendererFactory = (\n container: HTMLElement,\n) => ((target: HTMLElement) => void | (() => void)) | undefined;\n\n/**\n * Parse toolbar buttons container element (<tbw-grid-tool-buttons>).\n * This is a content container - we don't parse individual children.\n * The entire container content is registered as a single toolbar content entry.\n *\n * Example:\n * ```html\n * <tbw-grid>\n * <tbw-grid-tool-buttons>\n * <button>My button</button>\n * <button>My other button</button>\n * </tbw-grid-tool-buttons>\n * </tbw-grid>\n * ```\n *\n * The container's children are moved to the toolbar area during render.\n * We treat this as opaque content - users control what goes inside.\n *\n * @param host - The grid host element\n * @param state - Shell state to update\n * @param rendererFactory - Optional factory for creating renderers (used by framework adapters)\n */\nexport function parseLightDomToolButtons(\n host: HTMLElement,\n state: ShellState,\n rendererFactory?: ToolbarContentRendererFactory,\n): void {\n // Look for the toolbar buttons container element\n const toolButtonsContainer = host.querySelector(':scope > tbw-grid-tool-buttons') as HTMLElement | null;\n if (!toolButtonsContainer) return;\n\n // Mark that we found the container (for shouldRenderShellHeader)\n state.hasToolButtonsContainer = true;\n\n // Skip if already registered\n const id = 'light-dom-toolbar-content';\n if (state.lightDomToolbarContentIds.has(id)) return;\n\n // Register as a single content entry with a render function\n const adapterRenderer = rendererFactory?.(toolButtonsContainer);\n\n const contentDef: ToolbarContentDefinition = {\n id,\n order: 0, // Light DOM content comes first\n render:\n adapterRenderer ??\n ((target: HTMLElement) => {\n // Move all children from the light DOM container to the target\n while (toolButtonsContainer.firstChild) {\n target.appendChild(toolButtonsContainer.firstChild);\n }\n // Return cleanup that moves children back to original container\n // This preserves them across full re-renders that destroy the slot\n return () => {\n while (target.firstChild) {\n toolButtonsContainer.appendChild(target.firstChild);\n }\n };\n }),\n };\n\n state.toolbarContents.set(id, contentDef);\n state.lightDomToolbarContentIds.add(id);\n\n // Hide the original container\n toolButtonsContainer.style.display = 'none';\n}\n\n/**\n * Callback type for creating a tool panel renderer from a light DOM element.\n * This is used by framework adapters (Angular, React, etc.) to create renderers\n * from their template syntax.\n */\nexport type ToolPanelRendererFactory = (\n element: HTMLElement,\n) => ((container: HTMLElement) => void | (() => void)) | undefined;\n\n/**\n * Parse light DOM tool panel elements (<tbw-grid-tool-panel>).\n * These can appear as direct children of <tbw-grid> for declarative tool panel configuration.\n *\n * Attributes:\n * - `id` (required): Unique panel identifier\n * - `title` (required): Panel title shown in accordion header\n * - `icon`: Icon for accordion section header (emoji or text)\n * - `tooltip`: Tooltip for accordion section header\n * - `order`: Panel order priority (lower = first, default: 100)\n *\n * For vanilla JS, the element's innerHTML is used as the panel content.\n * For framework adapters, the adapter can provide a custom renderer factory.\n *\n * @param host - The grid host element\n * @param state - Shell state to update\n * @param rendererFactory - Optional factory for creating renderers (used by framework adapters)\n */\nexport function parseLightDomToolPanels(\n host: HTMLElement,\n state: ShellState,\n rendererFactory?: ToolPanelRendererFactory,\n): void {\n const toolPanelElements = host.querySelectorAll(':scope > tbw-grid-tool-panel');\n\n toolPanelElements.forEach((element) => {\n const panelEl = element as HTMLElement;\n const id = panelEl.getAttribute('id');\n const title = panelEl.getAttribute('title');\n\n // Skip if required attributes are missing\n if (!id || !title) {\n warnDiagnostic(\n TOOL_PANEL_MISSING_ATTR,\n `Tool panel missing required id or title attribute: id=\"${id ?? ''}\", title=\"${title ?? ''}\"`,\n );\n return;\n }\n\n const icon = panelEl.getAttribute('icon') ?? undefined;\n const tooltip = panelEl.getAttribute('tooltip') ?? undefined;\n const order = parseInt(panelEl.getAttribute('order') ?? '100', 10);\n\n // Try framework adapter first, then fall back to innerHTML\n let render: (container: HTMLElement) => void | (() => void);\n\n const adapterRenderer = rendererFactory?.(panelEl);\n if (adapterRenderer) {\n render = adapterRenderer;\n } else {\n // Vanilla fallback: use sanitized innerHTML as static content.\n // Light DOM authored markup is generally trusted, but we sanitize\n // defensively in case the panel content was server-rendered from data.\n const content = sanitizeHTML(panelEl.innerHTML.trim());\n render = (container: HTMLElement) => {\n const wrapper = document.createElement('div');\n wrapper.innerHTML = content;\n container.appendChild(wrapper);\n return () => wrapper.remove();\n };\n }\n\n // Check if panel was already parsed\n const existingPanel = state.toolPanels.get(id);\n\n // Precedence (#370): a panel contributed by a plugin (or registered via the\n // grid API) owns its id. A light-DOM <tbw-grid-tool-panel> with the same id\n // is ignored so the owner can rely on its own DOM existing. We detect a\n // foreign owner by the id being present in toolPanels but NOT tracked as a\n // light-DOM id (which would mean this is just a re-parse of our own panel).\n if (existingPanel && !state.lightDomToolPanelIds.has(id)) {\n warnDiagnostic(\n TOOL_PANEL_DUPLICATE,\n `Tool panel \"${id}\" is already provided by a plugin or the grid API; ignoring the matching light-DOM <tbw-grid-tool-panel>.`,\n );\n panelEl.style.display = 'none';\n return;\n }\n\n // If already parsed and we have an adapter renderer, refresh the render\n // function and (potentially) re-read attributes from DOM (Angular may have\n // updated them after the initial parse). However, **only tear down the\n // already-rendered panel content** when something the user can observe has\n // actually changed:\n // - First time the adapter renderer attaches (we may have been using\n // the vanilla innerHTML fallback up to now)\n // - Any attribute the panel header renders from changed\n //\n // Otherwise the user's React/Vue/Angular component is already mounted in\n // the panel and is happily managing its own state. A teardown here unmounts\n // it (losing local state) and re-mounts it on next renderPanelContent —\n // which, for a scrollable panel, resets `scrollTop` to 0. This was the\n // cause of the \"tool panel scroll jumps to top on column visibility toggle\"\n // bug: every `grid.gridConfig = …` setter in the adapter eventually calls\n // back into parseLightDomToolPanels (via #applyGridConfigUpdate), and the\n // previous unconditional `cleanup()` here threw away the user's component\n // tree on every config sync.\n if (existingPanel) {\n if (adapterRenderer) {\n const firstAdapterAttach = !state.adapterBoundToolPanelIds.has(id);\n const attrsChanged =\n existingPanel.order !== order || existingPanel.icon !== icon || existingPanel.tooltip !== tooltip;\n\n // Always keep the render function fresh — adapter renderers capture\n // the current grid element / portal context and the most recent\n // children, so a stale reference would render against an old closure.\n existingPanel.render = render;\n existingPanel.order = order;\n existingPanel.icon = icon;\n existingPanel.tooltip = tooltip;\n state.adapterBoundToolPanelIds.add(id);\n\n if (firstAdapterAttach || attrsChanged) {\n const cleanup = state.panelCleanups.get(id);\n if (cleanup) {\n cleanup();\n state.panelCleanups.delete(id);\n }\n }\n }\n return;\n }\n\n // Register the tool panel\n const panel: ToolPanelDefinition = {\n id,\n title,\n icon,\n tooltip,\n order,\n render,\n };\n\n state.toolPanels.set(id, panel);\n state.lightDomToolPanelIds.add(id);\n if (adapterRenderer) {\n state.adapterBoundToolPanelIds.add(id);\n }\n\n // Hide the light DOM element\n panelEl.style.display = 'none';\n });\n}\n// #endregion\n\n// #region Event Handlers\n/**\n * Set up event listeners for shell toolbar buttons and accordion.\n */\nexport function setupShellEventListeners(\n renderRoot: Element,\n config: ShellConfig | undefined,\n state: ShellState,\n callbacks: {\n onPanelToggle: () => void;\n onSectionToggle: (sectionId: string) => void;\n /** Invoked when the in-panel close (✕) button is clicked. */\n onPanelClose?: () => void;\n },\n): void {\n const toolbar = renderRoot.querySelector('.tbw-shell-toolbar');\n if (toolbar) {\n toolbar.addEventListener('click', (e) => {\n const target = e.target as HTMLElement;\n\n // Handle single panel toggle button\n const panelToggle = target.closest('[data-panel-toggle]') as HTMLElement | null;\n if (panelToggle) {\n callbacks.onPanelToggle();\n return;\n }\n });\n }\n\n // In-panel close (✕) button — only present when the header bar is hidden.\n const panel = renderRoot.querySelector('.tbw-tool-panel');\n if (panel && callbacks.onPanelClose) {\n panel.addEventListener('click', (e) => {\n const target = e.target as HTMLElement;\n if (target.closest('[data-panel-close]')) {\n callbacks.onPanelClose!();\n }\n });\n }\n\n // Accordion header clicks\n const accordion = renderRoot.querySelector('.tbw-accordion');\n if (accordion) {\n accordion.addEventListener('click', (e) => {\n const target = e.target as HTMLElement;\n const header = target.closest('.tbw-accordion-header') as HTMLElement | null;\n if (header) {\n const section = header.closest('[data-section]') as HTMLElement | null;\n const sectionId = section?.getAttribute('data-section');\n if (sectionId) {\n callbacks.onSectionToggle(sectionId);\n }\n }\n });\n }\n}\n\n/**\n * Whether `node` belongs to `gridElement` — directly (light DOM) or via the\n * grid's shadow root (the node's root-node host is the grid). Used to scope\n * click-outside checks so one grid's listener never treats another grid's\n * toggle or panel as \"inside\" itself.\n */\nfunction isInGrid(node: Node, gridElement: Element): boolean {\n if (gridElement.contains(node)) return true;\n const root = node.getRootNode();\n return root instanceof ShadowRoot && root.host === gridElement;\n}\n\n/**\n * Set up a click-outside listener that closes the tool panel when the user\n * clicks anywhere in the document outside the tool panel itself.\n *\n * Only active when `config.toolPanel.closeOnClickOutside` is `true` AND the\n * panel is open. The listener is added on `document` `mousedown` (not `click`)\n * so it fires before focus changes and catches clicks anywhere in the window —\n * including outside the grid. Uses `composedPath()` so clicks inside the\n * grid's shadow DOM are correctly attributed (shadow-retargeted `event.target`\n * would otherwise always be the host element).\n *\n * @returns A cleanup function that removes the listener.\n */\nexport function setupClickOutsideDismiss(\n gridElement: Element,\n config: ShellConfig | undefined,\n state: ShellState,\n onClose: () => void,\n): () => void {\n const mode = config?.toolPanel?.mode;\n\n // Dropdown popovers always light-dismiss on outside click (standard dropdown\n // behavior), regardless of the `closeOnClickOutside` flag. Other modes only\n // dismiss when the consumer opts in.\n if (mode !== 'dropdown' && !config?.toolPanel?.closeOnClickOutside) {\n // eslint-disable-next-line @typescript-eslint/no-empty-function\n return () => {};\n }\n\n // In push mode the panel does not overlap grid content, so there is\n // no meaningful \"outside\" to click against. Treat the option as a no-op.\n if (mode === 'push') {\n // eslint-disable-next-line @typescript-eslint/no-empty-function\n return () => {};\n }\n\n const handler = (e: Event) => {\n if (!state.isPanelOpen) return;\n\n // Ignore clicks inside the tool panel itself, on the toggle button, or on\n // the dropdown's resolved anchor element (so a consumer's BYO trigger can\n // toggle without the outside-click handler racing the button's own click).\n // composedPath() pierces shadow boundaries so this works for both the\n // light-DOM toggle and the shadow-DOM panel.\n //\n // CRITICAL: the panel/toggle match MUST be scoped to THIS grid via\n // `isInGrid`. With multiple grids on one page, a click on another grid's\n // toggle/panel is an \"outside\" click for us and must still dismiss \\u2014\n // otherwise opening a dropdown in grid B leaves grid A's dropdown stuck\n // open (#375 follow-up).\n const anchorEl = state.dropdownAnchorEl;\n for (const node of e.composedPath()) {\n if (anchorEl != null && node === anchorEl) return;\n if (\n node instanceof Element &&\n (node.classList?.contains('tbw-tool-panel') || node.matches?.('[data-panel-toggle]')) &&\n isInGrid(node, gridElement)\n ) {\n return;\n }\n }\n\n onClose();\n };\n\n document.addEventListener('mousedown', handler);\n return () => document.removeEventListener('mousedown', handler);\n}\n\n/**\n * Set up an Escape-key listener that closes an open overlay tool panel.\n *\n * Active only in `overlay` mode (a push panel is a persistent sidebar, not a\n * transient overlay, so Esc does not dismiss it — the in-panel close button is\n * the affordance there). Yields to more specific handlers: if another listener\n * already called `preventDefault()` on the keydown (e.g. an active cell editor\n * cancelling an edit), the panel is left open.\n *\n * @returns A cleanup function that removes the listener.\n */\nexport function setupEscapeDismiss(\n config: ShellConfig | undefined,\n state: ShellState,\n onClose: () => void,\n): () => void {\n // Push panels are persistent — Esc only dismisses overlay panels.\n if (config?.toolPanel?.mode === 'push') {\n // eslint-disable-next-line @typescript-eslint/no-empty-function\n return () => {};\n }\n\n const handler = (e: KeyboardEvent) => {\n if (e.key !== 'Escape' || e.defaultPrevented) return;\n if (!state.isPanelOpen) return;\n onClose();\n };\n\n document.addEventListener('keydown', handler);\n return () => document.removeEventListener('keydown', handler);\n}\n\n/**\n * Set up resize handle for tool panel.\n * Returns a cleanup function to remove event listeners.\n */\nexport function setupToolPanelResize(\n renderRoot: Element,\n config: ShellConfig | undefined,\n onResize: (width: number) => void,\n): () => void {\n const panel = renderRoot.querySelector('.tbw-tool-panel') as HTMLElement | null;\n const handle = renderRoot.querySelector('[data-resize-handle]') as HTMLElement | null;\n const shellBody = renderRoot.querySelector('.tbw-shell-body') as HTMLElement | null;\n if (!panel || !handle || !shellBody) {\n // eslint-disable-next-line @typescript-eslint/no-empty-function\n return () => {};\n }\n\n const position = config?.toolPanel?.position ?? 'right';\n const minWidth = 200;\n\n let startX = 0;\n let startWidth = 0;\n let maxWidth = 0;\n let isResizing = false;\n\n const onMouseMove = (e: MouseEvent) => {\n if (!isResizing) return;\n e.preventDefault();\n\n // For right-positioned panel: dragging left (negative clientX change) should expand\n // For left-positioned panel: dragging right (positive clientX change) should expand\n const delta = position === 'left' ? e.clientX - startX : startX - e.clientX;\n const newWidth = Math.min(maxWidth, Math.max(minWidth, startWidth + delta));\n\n panel.style.width = `${newWidth}px`;\n };\n\n const onMouseUp = () => {\n if (!isResizing) return;\n isResizing = false;\n handle.classList.remove('resizing');\n panel.style.transition = ''; // Re-enable transition\n document.body.style.cursor = '';\n document.body.style.userSelect = '';\n\n // Get final width and notify\n const finalWidth = panel.getBoundingClientRect().width;\n onResize(finalWidth);\n\n document.removeEventListener('mousemove', onMouseMove);\n document.removeEventListener('mouseup', onMouseUp);\n };\n\n const onMouseDown = (e: MouseEvent) => {\n e.preventDefault();\n isResizing = true;\n startX = e.clientX;\n startWidth = panel.getBoundingClientRect().width;\n // Calculate max width dynamically based on grid container width\n maxWidth = shellBody.getBoundingClientRect().width - 20; // Leave 20px margin\n handle.classList.add('resizing');\n panel.style.transition = 'none'; // Disable transition for smooth resize\n document.body.style.cursor = 'col-resize';\n document.body.style.userSelect = 'none';\n\n document.addEventListener('mousemove', onMouseMove);\n document.addEventListener('mouseup', onMouseUp);\n };\n\n handle.addEventListener('mousedown', onMouseDown);\n\n // Return cleanup function\n return () => {\n handle.removeEventListener('mousedown', onMouseDown);\n document.removeEventListener('mousemove', onMouseMove);\n document.removeEventListener('mouseup', onMouseUp);\n };\n}\n// #endregion\n\n// #region Content Rendering\n/**\n * Render toolbar content (render functions) into toolbar slots.\n * All contents (config + API + light DOM) are now unified.\n */\nexport function renderCustomToolbarContents(\n renderRoot: Element,\n config: ShellConfig | undefined,\n state: ShellState,\n): void {\n // Merge config contents with state contents (same logic as renderShellHeader)\n const configContents = config?.header?.toolbarContents ?? [];\n const stateContents = [...state.toolbarContents.values()];\n const configIds = new Set(configContents.map((c) => c.id));\n const allContents = [...configContents];\n for (const content of stateContents) {\n if (!configIds.has(content.id)) {\n allContents.push(content);\n }\n }\n\n // Only process contents that need rendering (have render and cleanup not already set)\n for (const content of allContents) {\n // Skip if already rendered (cleanup exists)\n if (state.toolbarContentCleanups.has(content.id)) continue;\n if (!content.render) continue;\n\n const slot = renderRoot.querySelector(`[data-toolbar-content=\"${content.id}\"]`);\n if (!slot) continue;\n\n const cleanup = content.render(slot as HTMLElement);\n if (cleanup) {\n state.toolbarContentCleanups.set(content.id, cleanup);\n }\n }\n}\n\n/**\n * Render header content from plugins into the shell content area.\n * Also moves light DOM header content to the placeholder (once).\n */\nexport function renderHeaderContent(renderRoot: Element, state: ShellState): void {\n // Early exit if nothing to do (most common path after initial render)\n const hasLightDomContent = state.lightDomHeaderContent.length > 0 && !state.lightDomContentMoved;\n const hasPluginContent = state.headerContents.size > 0;\n if (!hasLightDomContent && !hasPluginContent) return;\n\n const contentArea = renderRoot.querySelector('.tbw-shell-content');\n if (!contentArea) return;\n\n // Move light DOM header content to placeholder - only once (perf optimization)\n if (hasLightDomContent) {\n for (const el of state.lightDomHeaderContent) {\n el.style.display = ''; // Show it (was hidden in the original container)\n contentArea.appendChild(el);\n }\n state.lightDomContentMoved = true;\n }\n\n // Sort by order\n const sortedContents = [...state.headerContents.values()].sort((a, b) => (a.order ?? 100) - (b.order ?? 100));\n\n for (const content of sortedContents) {\n // Clean up previous render if any\n const existingCleanup = state.headerContentCleanups.get(content.id);\n if (existingCleanup) {\n existingCleanup();\n state.headerContentCleanups.delete(content.id);\n }\n\n // Check if container already exists\n let container = contentArea.querySelector(`[data-header-content=\"${content.id}\"]`) as HTMLElement | null;\n if (!container) {\n container = document.createElement('div');\n container.setAttribute('data-header-content', content.id);\n contentArea.appendChild(container);\n }\n\n const cleanup = content.render(container);\n if (cleanup) {\n state.headerContentCleanups.set(content.id, cleanup);\n }\n }\n}\n\n/**\n * Render content for expanded accordion sections.\n * @param icons - Optional icons for expand/collapse chevrons (from grid config)\n */\nexport function renderPanelContent(\n renderRoot: Element,\n state: ShellState,\n _icons?: { expand?: IconValue; collapse?: IconValue },\n): void {\n if (!state.isPanelOpen) return;\n\n for (const [panelId, panel] of state.toolPanels) {\n const isExpanded = state.expandedSections.has(panelId);\n const section = renderRoot.querySelector(`[data-section=\"${panelId}\"]`);\n const contentArea = section?.querySelector('.tbw-accordion-content') as HTMLElement | null;\n\n if (!section || !contentArea) continue;\n\n // Update expanded state\n section.classList.toggle('expanded', isExpanded);\n const header = section.querySelector('.tbw-accordion-header');\n if (header) {\n header.setAttribute('aria-expanded', String(isExpanded));\n }\n // Don't swap chevron icon — CSS rotation handles expanded/collapsed state\n\n if (isExpanded) {\n // Check if content is already rendered\n if (contentArea.children.length === 0) {\n // Render panel content\n const cleanup = panel.render(contentArea);\n if (cleanup) {\n state.panelCleanups.set(panelId, cleanup);\n }\n }\n } else {\n // Clean up and clear content when collapsed\n const cleanup = state.panelCleanups.get(panelId);\n if (cleanup) {\n cleanup();\n state.panelCleanups.delete(panelId);\n }\n contentArea.innerHTML = '';\n }\n }\n}\n\n/**\n * Update toolbar button active states.\n */\nexport function updateToolbarActiveStates(renderRoot: Element, state: ShellState): void {\n // Update single panel toggle button\n const panelToggle = renderRoot.querySelector('[data-panel-toggle]');\n if (panelToggle) {\n panelToggle.classList.toggle('active', state.isPanelOpen);\n panelToggle.setAttribute('aria-pressed', String(state.isPanelOpen));\n }\n}\n\n/**\n * Update tool panel open/close state.\n */\nexport function updatePanelState(renderRoot: Element, state: ShellState): void {\n const panel = renderRoot.querySelector('.tbw-tool-panel') as HTMLElement | null;\n if (!panel) return;\n\n panel.classList.toggle('open', state.isPanelOpen);\n\n // Clear inline width when closing (resize sets inline style that overrides CSS)\n if (!state.isPanelOpen) {\n panel.style.width = '';\n }\n}\n\n/** Base CSS anchor name used to feature-detect CSS anchor positioning support. */\nconst DROPDOWN_ANCHOR_NAME = '--tbw-tool-panel-anchor';\n\n/**\n * Monotonic counter for minting unique per-grid dropdown anchor names. A shared\n * constant name collides when two grids on the same page each open a dropdown,\n * pairing one grid's popover with the other grid's trigger (#375 follow-up).\n */\nlet dropdownAnchorSeq = 0;\n\n/** Whether the current environment supports CSS anchor positioning. */\nexport function supportsAnchorPositioning(): boolean {\n return (\n typeof CSS !== 'undefined' &&\n typeof CSS.supports === 'function' &&\n CSS.supports('anchor-name', DROPDOWN_ANCHOR_NAME)\n );\n}\n\n/**\n * Position the dropdown popover with fixed coordinates derived from the\n * anchor's bounding rect. Used only when CSS anchor positioning is\n * unsupported; otherwise placement is driven entirely by `shell.css`.\n */\nfunction positionDropdownFallback(panel: HTMLElement, anchorEl: HTMLElement, corner: boolean): void {\n const rect = anchorEl.getBoundingClientRect();\n const position = panel.getAttribute('data-position') ?? 'right';\n panel.style.position = 'fixed';\n // `corner` anchors to the top of the grid (top corner); otherwise drop below\n // the trigger button.\n panel.style.top = `${Math.round((corner ? rect.top : rect.bottom) + 4)}px`;\n if (position === 'left') {\n panel.style.left = `${Math.round(rect.left)}px`;\n panel.style.right = 'auto';\n } else {\n const viewportWidth = typeof window !== 'undefined' ? window.innerWidth : 0;\n panel.style.left = 'auto';\n panel.style.right = `${Math.round(viewportWidth - rect.right)}px`;\n }\n}\n\n/**\n * Show the tool panel as an anchored dropdown popover (`mode: 'dropdown'`).\n *\n * Reuses the existing `.tbw-tool-panel` aside (the full accordion) and shows it\n * in the top layer via the native Popover API. Placement is handled by CSS\n * anchor positioning when supported, with a JS bounding-rect fallback otherwise.\n *\n * @param anchorEl - Element the popover anchors to.\n * @param corner - When `true`, anchor to the top corner of `anchorEl` (grid\n * corner fallback); otherwise drop directly below it (toggle / custom button).\n */\nexport function showToolPanelDropdown(\n renderRoot: Element,\n state: ShellState,\n anchorEl: HTMLElement,\n corner: boolean,\n): void {\n const panel = renderRoot.querySelector('.tbw-tool-panel') as (HTMLElement & { showPopover?: () => void }) | null;\n if (!panel) return;\n\n state.dropdownAnchorEl = anchorEl;\n panel.dataset.anchor = corner ? 'corner' : 'below';\n\n // Show in the top layer. Guard for environments (older browsers / test DOM)\n // without the native Popover API.\n if (panel.hasAttribute('popover') && typeof panel.showPopover === 'function') {\n try {\n panel.showPopover();\n } catch {\n // Already open or not connected — ignore.\n }\n }\n\n if (supportsAnchorPositioning()) {\n // CSS drives placement; just wire up the anchor-name pairing. Mint a unique\n // name per show so multiple grids (or repeated opens) on one page never\n // collide on a shared anchor name — a collision positions this popover\n // against another grid's trigger instead of its own.\n const anchorName = `${DROPDOWN_ANCHOR_NAME}-${++dropdownAnchorSeq}`;\n state.dropdownAnchorName = anchorName;\n anchorEl.style.setProperty('anchor-name', anchorName);\n panel.style.setProperty('position-anchor', anchorName);\n panel.style.removeProperty('top');\n panel.style.removeProperty('left');\n panel.style.removeProperty('right');\n } else {\n positionDropdownFallback(panel, anchorEl, corner);\n }\n}\n\n/**\n * Hide the dropdown popover and clear all anchor wiring set by\n * {@link showToolPanelDropdown}.\n */\nexport function hideToolPanelDropdown(renderRoot: Element, state: ShellState): void {\n const panel = renderRoot.querySelector('.tbw-tool-panel') as (HTMLElement & { hidePopover?: () => void }) | null;\n if (panel) {\n if (panel.hasAttribute('popover') && typeof panel.hidePopover === 'function') {\n try {\n panel.hidePopover();\n } catch {\n // Not currently open — ignore.\n }\n }\n panel.style.removeProperty('position-anchor');\n panel.style.removeProperty('position');\n panel.style.removeProperty('top');\n panel.style.removeProperty('left');\n panel.style.removeProperty('right');\n delete panel.dataset.anchor;\n }\n if (state.dropdownAnchorEl) {\n state.dropdownAnchorEl.style.removeProperty('anchor-name');\n state.dropdownAnchorEl = null;\n }\n state.dropdownAnchorName = null;\n}\n\n/**\n * Re-pair an OPEN dropdown's existing anchor onto a freshly resolved trigger\n * IN PLACE — reusing the already-minted `anchor-name` and leaving the popover\n * shown — instead of hiding and re-showing it.\n *\n * WHY: when an outer framework (e.g. a React column `headerRenderer`) recreates\n * the `[data-panel-toggle]` trigger on a column toggle, the new node lacks the\n * `anchor-name`, so the popover's `position-anchor` no longer resolves and the\n * browser drops it to the corner. Hiding + re-showing (the heavier\n * {@link showToolPanelDropdown} path) mints a NEW name and re-enters the\n * Popover API, which flashes the popover to the corner for a frame. Moving the\n * SAME name to the new node keeps the popover visually anchored with no flash.\n *\n * Returns `true` when the re-pair was applied. The caller must ensure CSS\n * anchor positioning is supported (see {@link supportsAnchorPositioning});\n * otherwise the fixed-coordinate fallback in {@link showToolPanelDropdown} must\n * be used instead.\n */\nexport function repairDropdownAnchor(renderRoot: Element, state: ShellState, trigger: HTMLElement): boolean {\n const anchorName = state.dropdownAnchorName;\n if (!anchorName) return false;\n const panel = renderRoot.querySelector('.tbw-tool-panel') as (HTMLElement & { showPopover?: () => void }) | null;\n if (!panel) return false;\n const prev = state.dropdownAnchorEl;\n if (prev && prev !== trigger) prev.style.removeProperty('anchor-name');\n trigger.style.setProperty('anchor-name', anchorName);\n panel.style.setProperty('position-anchor', anchorName);\n panel.style.removeProperty('top');\n panel.style.removeProperty('left');\n panel.style.removeProperty('right');\n panel.dataset.anchor = 'below';\n state.dropdownAnchorEl = trigger;\n // A structural rebuild (`rebuildShellDOM`) creates a FRESH `.tbw-tool-panel`\n // that is not in the top layer — only its `.open` class arm renders it, which\n // can be clipped / mis-stacked. Re-assert the popover idempotently so both the\n // non-structural path (panel already open → no-op via the catch) and the\n // structural-rebuild path (fresh panel → shown) end up in the top layer.\n if (panel.hasAttribute('popover') && typeof panel.showPopover === 'function') {\n try {\n panel.showPopover();\n } catch {\n // Already open or not connected — ignore.\n }\n }\n return true;\n}\n\n/**\n * Prepare shell state for a full re-render.\n * Runs cleanup functions so content (toolbar buttons, panels, headers)\n * can be restored to their original containers and re-rendered into new DOM.\n */\nexport function prepareForRerender(state: ShellState): void {\n // Run cleanups for toolbar contents (moves elements back to original containers)\n for (const cleanup of state.toolbarContentCleanups.values()) {\n cleanup();\n }\n state.toolbarContentCleanups.clear();\n\n // Run cleanups for panel contents (old DOM will be destroyed)\n for (const cleanup of state.panelCleanups.values()) {\n cleanup();\n }\n state.panelCleanups.clear();\n\n // Run cleanups for header contents (old DOM will be destroyed)\n for (const cleanup of state.headerContentCleanups.values()) {\n cleanup();\n }\n state.headerContentCleanups.clear();\n\n // Allow light DOM content to be re-moved into new DOM\n state.lightDomContentMoved = false;\n}\n\n/**\n * Cleanup all shell state when grid disconnects.\n */\nexport function cleanupShellState(state: ShellState): void {\n // Clean up header content\n for (const cleanup of state.headerContentCleanups.values()) {\n cleanup();\n }\n state.headerContentCleanups.clear();\n\n // Clean up panel content\n for (const cleanup of state.panelCleanups.values()) {\n cleanup();\n }\n state.panelCleanups.clear();\n\n // Clean up toolbar contents\n for (const cleanup of state.toolbarContentCleanups.values()) {\n cleanup();\n }\n state.toolbarContentCleanups.clear();\n\n // Call onDestroy for all toolbar contents\n for (const content of state.toolbarContents.values()) {\n content.onDestroy?.();\n }\n\n // Invoke onClose for all open panels\n if (state.isPanelOpen) {\n for (const sectionId of state.expandedSections) {\n const panel = state.toolPanels.get(sectionId);\n panel?.onClose?.();\n }\n }\n\n // Reset panel state\n state.isPanelOpen = false;\n state.expandedSections.clear();\n\n // Clear registrations\n state.toolPanels.clear();\n state.headerContents.clear();\n state.toolbarContents.clear();\n state.lightDomHeaderContent = [];\n\n // Clear light DOM tracking sets (allow re-parsing)\n state.lightDomToolPanelIds.clear();\n state.adapterBoundToolPanelIds.clear();\n state.lightDomToolbarContentIds.clear();\n\n // Reset move tracking flag (allow re-initialization)\n state.lightDomContentMoved = false;\n}\n// #endregion\n\n// #region Grid HTML Templates\n/**\n * Core grid content HTML template.\n * Uses faux scrollbar pattern for smooth virtualized scrolling.\n */\nexport const GRID_CONTENT_HTML = `\n <div class=\"tbw-scroll-area\">\n <div class=\"rows-body-wrapper\">\n <div class=\"rows-body\" role=\"grid\">\n <div class=\"header\" role=\"rowgroup\">\n <div class=\"header-row\" role=\"row\" part=\"header-row\"></div>\n </div>\n <div class=\"rows-container\" role=\"presentation\">\n <div class=\"rows-viewport\" role=\"presentation\">\n <div class=\"rows\"></div>\n </div>\n </div>\n </div>\n </div>\n </div>\n <div class=\"faux-vscroll\">\n <div class=\"faux-vscroll-spacer\"></div>\n </div>\n <div class=\"tbw-sr-only\" aria-live=\"polite\" aria-atomic=\"true\"></div>\n`;\n// #endregion\n\n// #region DOM Construction\nimport { GridClasses } from '../../core/constants';\nimport {\n buildGridDOM,\n buildShellBody,\n buildShellHeader,\n type ShellBodyOptions,\n type ShellHeaderOptions,\n} from '../../core/internal/dom-builder';\n\n/**\n * Build ShellHeaderOptions and ShellBodyOptions from shell config and runtime state.\n * Shared by buildGridDOMIntoElement and rebuildShellDOM to avoid duplication.\n */\nfunction buildShellOptions(\n shellConfig: ShellConfig,\n runtimeState: { isPanelOpen: boolean; expandedSections: Set<string> },\n icons?: { toolPanel?: IconValue; expand?: IconValue; collapse?: IconValue },\n): { headerOptions: ShellHeaderOptions; bodyOptions: ShellBodyOptions } {\n const toolPanelIcon = icons?.toolPanel !== undefined ? iconToString(icons.toolPanel) : undefined;\n const expandIcon = icons?.expand !== undefined ? iconToString(icons.expand) : undefined;\n const collapseIcon = icons?.collapse !== undefined ? iconToString(icons.collapse) : undefined;\n\n const allContents = shellConfig.header?.toolbarContents ?? [];\n const sortedContents = [...allContents].sort((a, b) => (a.order ?? 0) - (b.order ?? 0));\n\n const allPanels = shellConfig.toolPanels ?? [];\n const sortedPanels = [...allPanels].sort((a, b) => (a.order ?? 100) - (b.order ?? 100));\n\n return {\n headerOptions: {\n title: shellConfig.header?.title ?? undefined,\n hasPanels: sortedPanels.length > 0,\n isPanelOpen: runtimeState.isPanelOpen,\n toolPanelIcon,\n showToggle: shellConfig.header?.toolPanelToggle !== false && shellConfig.toolPanel?.locked !== true,\n configButtons: sortedContents.map((c) => ({\n id: c.id,\n hasElement: false,\n hasRender: !!c.render,\n })),\n apiButtons: [],\n },\n bodyOptions: {\n position: shellConfig.toolPanel?.position ?? 'right',\n mode: shellConfig.toolPanel?.mode ?? 'overlay',\n isPanelOpen: runtimeState.isPanelOpen,\n expandIcon,\n collapseIcon,\n // Render an in-panel close (✕) button when the header bar is hidden, so\n // the panel can be dismissed without the (absent) built-in toggle. A\n // locked panel cannot be closed, so no button in that case. Dropdown mode\n // is excluded: the popover light-dismisses on Esc / click-outside, so a\n // ✕ would be redundant clutter.\n showCloseButton:\n shellConfig.header?.visible === false &&\n shellConfig.toolPanel?.locked !== true &&\n (shellConfig.toolPanel?.mode ?? 'overlay') !== 'dropdown',\n panels: sortedPanels.map((p) => ({\n id: p.id,\n title: p.title,\n icon: iconToString(p.icon),\n isExpanded: runtimeState.expandedSections.has(p.id),\n })),\n },\n };\n}\n\n/**\n * Build the complete grid DOM structure using direct DOM construction.\n * This is 2-3x faster than innerHTML for initial render.\n *\n * @param renderRoot - The element to render into (will be cleared)\n * @param shellConfig - Shell configuration\n * @param runtimeState - Runtime shell state\n * @param icons - Optional icons\n * @returns Whether shell is active (for post-render setup)\n */\nexport function buildGridDOMIntoElement(\n renderRoot: Element,\n shellConfig: ShellConfig | undefined,\n runtimeState: { isPanelOpen: boolean; expandedSections: Set<string> },\n icons?: { toolPanel?: IconValue; expand?: IconValue; collapse?: IconValue },\n): boolean {\n const hasShell = shouldRenderShellHeader(shellConfig);\n\n // Preserve all direct child nodes EXCEPT the grid's own `.tbw-grid-root`\n // (which we are about to rebuild). This keeps user/framework light-DOM\n // intact in its original order — including non-element nodes like comment\n // anchors that Angular's `@if`/`@for` and other structural directives use\n // as positional markers. Filtering by element-tag selectors would discard\n // those anchors and break re-renders of the conditional content.\n const preservedNodes: Node[] = [];\n for (let n: Node | null = renderRoot.firstChild; n; n = n.nextSibling) {\n if (n.nodeType === 1 && (n as Element).classList.contains(GridClasses.ROOT)) continue;\n preservedNodes.push(n);\n }\n\n // Clear existing content (this would delete light DOM nodes, so we preserved them first)\n renderRoot.replaceChildren();\n\n // Re-append preserved nodes in original order (hidden elements are used for config).\n // IMPORTANT: These are prepended before .tbw-grid-root, so `renderRoot.children[0]`\n // is NOT the grid root. Use `querySelector('.tbw-grid-root')` instead.\n for (const n of preservedNodes) {\n renderRoot.appendChild(n);\n }\n\n if (hasShell) {\n const { headerOptions, bodyOptions } = buildShellOptions(shellConfig!, runtimeState, icons);\n const shellHeader = shouldRenderHeaderBar(shellConfig) ? buildShellHeader(headerOptions) : undefined;\n const shellBody = buildShellBody(bodyOptions);\n\n const fragment = buildGridDOM({\n hasShell: true,\n shellHeader,\n shellBody,\n });\n renderRoot.appendChild(fragment);\n } else {\n const fragment = buildGridDOM({ hasShell: false });\n renderRoot.appendChild(fragment);\n }\n\n return hasShell;\n}\n\n/**\n * Surgically rebuild only the shell wrapper (header + tool panel) while\n * preserving the existing `.tbw-grid-root` element and its `.tbw-grid-content`\n * child with all descendants and event listeners intact.\n *\n * This avoids the full `replaceChildren()` nuke that `buildGridDOMIntoElement`\n * performs, so event listeners bound to `.tbw-grid-root` or its grid content\n * descendants (e.g. tooltip's delegated mouseover) remain intact.\n *\n * If no existing `.tbw-grid-root` is found (first render), falls back to\n * `buildGridDOMIntoElement` for a full rebuild.\n */\nexport function rebuildShellDOM(\n renderRoot: Element,\n shellConfig: ShellConfig | undefined,\n runtimeState: { isPanelOpen: boolean; expandedSections: Set<string> },\n icons?: { toolPanel?: IconValue; expand?: IconValue; collapse?: IconValue },\n): boolean {\n // Find the existing grid root and content to preserve\n const existingRoot = renderRoot.querySelector(`.${GridClasses.ROOT}`) as HTMLElement | null;\n const existingContent = existingRoot?.querySelector('.tbw-grid-content');\n\n // If there's no existing root, fall back to full rebuild (first render)\n if (!existingRoot || !existingContent) {\n return buildGridDOMIntoElement(renderRoot, shellConfig, runtimeState, icons);\n }\n\n // Detach grid content so it survives the child replacement\n existingContent.remove();\n\n const hasShell = shouldRenderShellHeader(shellConfig);\n\n // Clear the root's children (shell header, shell body, etc.) but keep the root element itself\n existingRoot.replaceChildren();\n\n if (hasShell) {\n existingRoot.className = `${GridClasses.ROOT} has-shell`;\n\n const { headerOptions, bodyOptions } = buildShellOptions(shellConfig!, runtimeState, icons);\n const shellHeader = shouldRenderHeaderBar(shellConfig) ? buildShellHeader(headerOptions) : undefined;\n const shellBody = buildShellBody(bodyOptions);\n\n // Replace the freshly cloned grid content with the preserved one\n const freshContent = shellBody.querySelector('.tbw-grid-content');\n if (freshContent) {\n freshContent.replaceWith(existingContent);\n }\n\n if (shellHeader) existingRoot.appendChild(shellHeader);\n existingRoot.appendChild(shellBody);\n } else {\n // No shell — place content directly in root\n existingRoot.className = GridClasses.ROOT;\n existingRoot.appendChild(existingContent);\n }\n\n return hasShell;\n}\n// #endregion\n","/**\n * Shell Controller - Tool panel and header content orchestration.\n *\n * Extracted from shell.ts to isolate the stateful controller logic\n * from the rendering/parsing infrastructure.\n *\n * The ShellController manages:\n * - Tool panel open/close/toggle state\n * - Accordion section expand/collapse\n * - Registration of tool panels, header content, and toolbar content\n */\n\nimport {\n HEADER_CONTENT_DUPLICATE,\n NO_TOOL_PANELS,\n TOOL_PANEL_DUPLICATE,\n TOOL_PANEL_NOT_FOUND,\n TOOLBAR_CONTENT_DUPLICATE,\n warnDiagnostic,\n} from '../../core/internal/diagnostics';\nimport type { GridElement } from '../../core/plugin/base-plugin';\nimport type { InternalGrid } from '../../core/types';\nimport {\n hideToolPanelDropdown,\n renderHeaderContent,\n renderPanelContent,\n repairDropdownAnchor,\n showToolPanelDropdown,\n supportsAnchorPositioning,\n updatePanelState,\n updateToolbarActiveStates,\n type ShellState,\n} from './shell';\nimport type {\n HeaderContentDefinition,\n OpenToolPanelOptions,\n ToolbarContentDefinition,\n ToolPanelDefinition,\n} from './types';\n\n// #region ShellController\n/**\n * Controller interface for managing shell/tool panel behavior.\n */\nexport interface ShellController {\n /** Whether the shell has been initialized */\n readonly isInitialized: boolean;\n /** Set the initialized state */\n setInitialized(value: boolean): void;\n /** Whether the tool panel is currently open */\n readonly isPanelOpen: boolean;\n /** Get IDs of expanded accordion sections */\n readonly expandedSections: string[];\n /**\n * Open the tool panel.\n * @param panelId - Optional ID of the section to expand on open. When provided,\n * takes precedence over `shell.toolPanel.defaultOpen`. If the panel is already\n * open with a different section expanded, switches to the requested section.\n * If the panel ID is not registered, a `TBW072` warning is emitted and the\n * call falls back to default behavior (auto-expand `defaultOpen` or first by order).\n * @param options - Optional open options. In `mode: 'dropdown'`, `options.anchor`\n * sets the element the popover anchors to (see {@link OpenToolPanelOptions}).\n */\n openToolPanel(panelId?: string, options?: OpenToolPanelOptions): void;\n /** Close the tool panel */\n closeToolPanel(): void;\n /**\n * Toggle the tool panel.\n * @param options - Optional open options forwarded to {@link openToolPanel}\n * when the panel is currently closed (e.g. `anchor` for dropdown mode).\n */\n toggleToolPanel(options?: OpenToolPanelOptions): void;\n /**\n * Re-pair an open dropdown popover onto its (possibly recreated) trigger\n * after a re-render (`mode: 'dropdown'` only). A column toggle / reorder may\n * recreate the custom column-header button the popover was anchored to,\n * leaving the popover's `position-anchor` dangling so the browser drops it to\n * the corner.\n *\n * Behaviour:\n * - intact pairing → no-op (cheap fast-path, safe to call every render);\n * - a live `[data-panel-toggle]` exists → re-pair the existing anchor onto it\n * IN PLACE (no popover re-show → no flash), or a fixed-coord re-show when\n * CSS anchor positioning is unavailable;\n * - corner-origin dropdown with no trigger → keep it pinned to the corner;\n * - trigger-origin dropdown whose trigger is transiently absent → leave the\n * popover at its last position (the shell's MutationObserver re-pairs it\n * before paint once the trigger reappears), rather than flashing to corner.\n *\n * No-op when the panel is closed or the mode is not `'dropdown'`.\n */\n reanchorOpenDropdown(): void;\n /** Toggle an accordion section */\n toggleToolPanelSection(sectionId: string): void;\n /** Get registered tool panels */\n getToolPanels(): ToolPanelDefinition[];\n /** Register a tool panel */\n registerToolPanel(panel: ToolPanelDefinition): void;\n /** Unregister a tool panel */\n unregisterToolPanel(panelId: string): void;\n /** Get registered header contents */\n getHeaderContents(): HeaderContentDefinition[];\n /** Register header content */\n registerHeaderContent(content: HeaderContentDefinition): void;\n /** Unregister header content */\n unregisterHeaderContent(contentId: string): void;\n /** Get all registered toolbar contents */\n getToolbarContents(): ToolbarContentDefinition[];\n /** Register toolbar content */\n registerToolbarContent(content: ToolbarContentDefinition): void;\n /** Unregister toolbar content */\n unregisterToolbarContent(contentId: string): void;\n}\n\n/**\n * The slice of the grid the shell controller needs: the plugin-facing\n * {@link GridElement} surface plus a few internal seams (`id`, `_emit`,\n * `_accordionIcons`, optional `refreshShellHeader`). Declaring it as\n * `GridElement & Pick<InternalGrid, …>` lets the {@link ShellPlugin} pass its\n * `GridElement`-typed `grid` with a plain downcast — no `as unknown as` — while\n * keeping core free of any shell-specific grid surface (extraction #370).\n */\nexport type ShellControllerGrid = GridElement &\n Pick<InternalGrid, 'id' | '_emit' | '_accordionIcons' | 'refreshShellHeader'>;\n\n/**\n * Create a ShellController instance.\n * The controller encapsulates all tool panel orchestration logic.\n */\nexport function createShellController(state: ShellState, grid: ShellControllerGrid): ShellController {\n let initialized = false;\n\n /**\n * Resolve the element a dropdown popover should anchor to. Priority:\n * explicit `anchor` option → built-in toolbar toggle button → grid host\n * corner fallback.\n */\n function resolveDropdownAnchor(explicit?: HTMLElement): { el: HTMLElement; corner: boolean } {\n if (explicit) return { el: explicit, corner: false };\n const toggle = grid._renderRoot.querySelector('[data-panel-toggle]') as HTMLElement | null;\n if (toggle) return { el: toggle, corner: false };\n return { el: grid._hostElement, corner: true };\n }\n\n const controller: ShellController = {\n get isInitialized() {\n return initialized;\n },\n setInitialized(value: boolean) {\n initialized = value;\n },\n\n get isPanelOpen() {\n return state.isPanelOpen;\n },\n\n get expandedSections() {\n return [...state.expandedSections];\n },\n\n openToolPanel(panelId?: string, options?: OpenToolPanelOptions) {\n if (state.toolPanels.size === 0) {\n warnDiagnostic(NO_TOOL_PANELS, 'No tool panels registered', grid.id);\n return;\n }\n\n // Validate explicit panelId; on unknown id, warn and fall back to default behavior.\n let targetSectionId: string | undefined;\n if (panelId !== undefined) {\n if (state.toolPanels.has(panelId)) {\n targetSectionId = panelId;\n } else {\n warnDiagnostic(TOOL_PANEL_NOT_FOUND, `Tool panel \"${panelId}\" not found`, grid.id);\n }\n }\n\n // Fast path: panel already open and requested section already expanded → no-op.\n if (state.isPanelOpen && targetSectionId && state.expandedSections.has(targetSectionId)) {\n return;\n }\n\n // Panel already open but a different section is expanded → switch sections\n // (delegate to toggleToolPanelSection for accordion exclusivity + event emission).\n if (state.isPanelOpen && targetSectionId && !state.expandedSections.has(targetSectionId)) {\n controller.toggleToolPanelSection(targetSectionId);\n return;\n }\n\n if (state.isPanelOpen) return;\n\n state.isPanelOpen = true;\n\n // Decide initial expanded section:\n // 1. Explicit panelId argument (already validated above).\n // 2. shell.toolPanel.defaultOpen if it matches a registered panel.\n // 3. First panel by `order`.\n if (state.expandedSections.size === 0 && state.toolPanels.size > 0) {\n if (targetSectionId) {\n state.expandedSections.add(targetSectionId);\n } else {\n const defaultOpenId = grid.effectiveConfig?.shell?.toolPanel?.defaultOpen;\n if (defaultOpenId && state.toolPanels.has(defaultOpenId)) {\n state.expandedSections.add(defaultOpenId);\n } else {\n const sortedPanels = [...state.toolPanels.values()].sort((a, b) => (a.order ?? 100) - (b.order ?? 100));\n const firstPanel = sortedPanels[0];\n if (firstPanel) {\n state.expandedSections.add(firstPanel.id);\n }\n }\n }\n } else if (targetSectionId && !state.expandedSections.has(targetSectionId)) {\n // Some other section was already pre-expanded (e.g. via config); switch to requested.\n for (const id of [...state.expandedSections]) {\n state.expandedSections.delete(id);\n }\n state.expandedSections.add(targetSectionId);\n }\n\n // Update UI\n const shadow = grid._renderRoot;\n updateToolbarActiveStates(shadow, state);\n updatePanelState(shadow, state);\n\n // Render accordion sections\n renderPanelContent(shadow, state, grid._accordionIcons);\n\n // In dropdown mode, show the panel as an anchored popover in the top layer.\n if (grid.effectiveConfig?.shell?.toolPanel?.mode === 'dropdown') {\n const { el, corner } = resolveDropdownAnchor(options?.anchor);\n showToolPanelDropdown(shadow, state, el, corner);\n }\n\n // Emit event\n grid._emit('tool-panel-open', { sections: controller.expandedSections });\n },\n\n closeToolPanel() {\n if (!state.isPanelOpen) return;\n // When the tool panel is locked open, programmatic and user close\n // requests become no-ops. Accordion sections can still toggle.\n if (grid.effectiveConfig?.shell?.toolPanel?.locked) return;\n\n // Clean up all panel content\n for (const cleanup of state.panelCleanups.values()) {\n cleanup();\n }\n state.panelCleanups.clear();\n\n // Call onClose for all panels\n for (const panel of state.toolPanels.values()) {\n panel.onClose?.();\n }\n\n state.isPanelOpen = false;\n\n // Update UI\n const shadow = grid._renderRoot;\n updateToolbarActiveStates(shadow, state);\n updatePanelState(shadow, state);\n\n // In dropdown mode, hide the popover and clear anchor wiring.\n if (grid.effectiveConfig?.shell?.toolPanel?.mode === 'dropdown') {\n hideToolPanelDropdown(shadow, state);\n }\n\n // Emit event\n grid._emit('tool-panel-close', {});\n },\n\n toggleToolPanel(options?: OpenToolPanelOptions) {\n if (state.isPanelOpen) {\n controller.closeToolPanel();\n } else {\n controller.openToolPanel(undefined, options);\n }\n },\n\n reanchorOpenDropdown() {\n if (!state.isPanelOpen) return;\n if (grid.effectiveConfig?.shell?.toolPanel?.mode !== 'dropdown') return;\n const stored = state.dropdownAnchorEl;\n // Idempotent fast-path: the anchor element is still connected AND still\n // carries the minted `anchor-name`, so the pairing is intact — nothing to\n // do. This keeps the hook cheap when it runs on the `afterRender` path\n // (which fires on every render, e.g. a column toggle), only doing real\n // work when the trigger was actually recreated/detached.\n if (\n stored &&\n stored.isConnected &&\n state.dropdownAnchorName &&\n stored.style.getPropertyValue('anchor-name') === state.dropdownAnchorName\n ) {\n return;\n }\n // Idempotent fast-path (fixed-coordinate fallback mode — CSS anchor\n // positioning unsupported, so `dropdownAnchorName` is never minted). Here\n // re-anchoring means a `getBoundingClientRect()` + re-show on every\n // render/mutation, which is wasteful when nothing was recreated. Skip when\n // the stored trigger is still the SAME connected node (not the corner\n // anchor) AND the CURRENT panel was already positioned (`data-anchor` set).\n // A structural rebuild produces a FRESH `.tbw-tool-panel` with no\n // `data-anchor`, so this never short-circuits the case that still needs a\n // re-show.\n if (!state.dropdownAnchorName && stored && stored.isConnected && stored !== grid._hostElement) {\n const panel = grid._renderRoot.querySelector('.tbw-tool-panel') as HTMLElement | null;\n if (panel?.dataset.anchor === 'below') return;\n }\n // Re-pair to a LIVE custom trigger (`[data-panel-toggle]`) whenever one\n // exists. Prefer the cheap in-place repair: it reuses the already-minted\n // `anchor-name`, keeps the popover shown, and never re-enters the Popover\n // API — so the popover stays visually anchored with NO flash. Falls back\n // to a fixed-coordinate re-show only when CSS anchor positioning is\n // unavailable.\n const fresh = grid._renderRoot.querySelector('[data-panel-toggle]') as HTMLElement | null;\n if (fresh && fresh !== grid._hostElement) {\n if (\n state.dropdownAnchorName &&\n supportsAnchorPositioning() &&\n repairDropdownAnchor(grid._renderRoot, state, fresh)\n ) {\n return;\n }\n showToolPanelDropdown(grid._renderRoot, state, fresh, false);\n return;\n }\n // No live trigger.\n if (stored === grid._hostElement) {\n // Corner-origin dropdown (opened without a custom trigger): keep it\n // pinned to the grid corner across re-renders.\n showToolPanelDropdown(grid._renderRoot, state, grid._hostElement, true);\n return;\n }\n // Trigger-origin dropdown whose trigger is transiently absent (an outer\n // framework, e.g. React, recreates it on a separate commit) — or was\n // permanently removed. Do NOT flash the popover to the corner: leave it\n // at its last position. When the trigger reappears, the shell's\n // MutationObserver re-pairs it before paint (see ShellPlugin).\n },\n\n toggleToolPanelSection(sectionId: string) {\n const panel = state.toolPanels.get(sectionId);\n if (!panel) {\n warnDiagnostic(TOOL_PANEL_NOT_FOUND, `Tool panel section \"${sectionId}\" not found`, grid.id);\n return;\n }\n\n // Don't allow toggling when there's only one panel (it should stay expanded)\n if (state.toolPanels.size === 1) {\n return;\n }\n\n const shadow = grid._renderRoot;\n const isExpanded = state.expandedSections.has(sectionId);\n\n if (isExpanded) {\n // Collapsing current section\n const cleanup = state.panelCleanups.get(sectionId);\n if (cleanup) {\n cleanup();\n state.panelCleanups.delete(sectionId);\n }\n panel.onClose?.();\n state.expandedSections.delete(sectionId);\n updateAccordionSectionState(shadow, sectionId, false);\n } else {\n // Expanding - first collapse all others (exclusive accordion)\n for (const [otherId, otherPanel] of state.toolPanels) {\n if (otherId !== sectionId && state.expandedSections.has(otherId)) {\n const cleanup = state.panelCleanups.get(otherId);\n if (cleanup) {\n cleanup();\n state.panelCleanups.delete(otherId);\n }\n otherPanel.onClose?.();\n state.expandedSections.delete(otherId);\n updateAccordionSectionState(shadow, otherId, false);\n // Clear content of collapsed section\n const contentEl = shadow.querySelector(`[data-section=\"${otherId}\"] .tbw-accordion-content`);\n if (contentEl) contentEl.innerHTML = '';\n }\n }\n // Now expand the target section\n state.expandedSections.add(sectionId);\n updateAccordionSectionState(shadow, sectionId, true);\n renderAccordionSectionContent(shadow, state, sectionId);\n }\n\n // Emit event\n grid._emit('tool-panel-section-toggle', { id: sectionId, expanded: !isExpanded });\n },\n\n getToolPanels() {\n return [...state.toolPanels.values()];\n },\n\n registerToolPanel(panel: ToolPanelDefinition) {\n if (state.toolPanels.has(panel.id)) {\n warnDiagnostic(TOOL_PANEL_DUPLICATE, `Tool panel \"${panel.id}\" already registered`, grid.id);\n return;\n }\n state.toolPanels.set(panel.id, panel);\n\n // Refresh when already rendered, OR bootstrap the chrome when the shell is\n // connected but has not rendered yet (an opt-in `features: { shell: true }`\n // shell with no other content renders no chrome — registering the first\n // tool panel must build it). `refreshShellHeader` self-guards on\n // `isConnected` and batches, so a pre-connect call is a harmless no-op.\n if (initialized || grid._hostElement.isConnected) {\n grid.refreshShellHeader?.();\n }\n },\n\n unregisterToolPanel(panelId: string) {\n // Close panel if open and this section is expanded\n if (state.expandedSections.has(panelId)) {\n const cleanup = state.panelCleanups.get(panelId);\n if (cleanup) {\n cleanup();\n state.panelCleanups.delete(panelId);\n }\n state.expandedSections.delete(panelId);\n }\n\n state.toolPanels.delete(panelId);\n\n if (initialized || grid._hostElement.isConnected) {\n grid.refreshShellHeader?.();\n }\n },\n\n getHeaderContents() {\n return [...state.headerContents.values()];\n },\n\n registerHeaderContent(content: HeaderContentDefinition) {\n if (state.headerContents.has(content.id)) {\n warnDiagnostic(HEADER_CONTENT_DUPLICATE, `Header content \"${content.id}\" already registered`, grid.id);\n return;\n }\n state.headerContents.set(content.id, content);\n\n if (initialized) {\n renderHeaderContent(grid._renderRoot, state);\n } else if (grid._hostElement.isConnected) {\n // Connected opt-in shell with no chrome yet — bootstrap it so the header\n // content has a `.tbw-shell-header` to render into.\n grid.refreshShellHeader?.();\n }\n },\n\n unregisterHeaderContent(contentId: string) {\n // Clean up\n const cleanup = state.headerContentCleanups.get(contentId);\n if (cleanup) {\n cleanup();\n state.headerContentCleanups.delete(contentId);\n }\n\n // Call onDestroy\n const content = state.headerContents.get(contentId);\n content?.onDestroy?.();\n\n state.headerContents.delete(contentId);\n\n // Remove DOM element\n const el = grid._renderRoot.querySelector(`[data-header-content=\"${contentId}\"]`);\n el?.remove();\n },\n\n getToolbarContents() {\n return [...state.toolbarContents.values()].sort((a, b) => (a.order ?? 0) - (b.order ?? 0));\n },\n\n registerToolbarContent(content: ToolbarContentDefinition) {\n if (state.toolbarContents.has(content.id)) {\n warnDiagnostic(TOOLBAR_CONTENT_DUPLICATE, `Toolbar content \"${content.id}\" already registered`, grid.id);\n return;\n }\n state.toolbarContents.set(content.id, content);\n\n if (initialized || grid._hostElement.isConnected) {\n grid.refreshShellHeader?.();\n }\n },\n\n unregisterToolbarContent(contentId: string) {\n // Clean up\n const cleanup = state.toolbarContentCleanups.get(contentId);\n if (cleanup) {\n cleanup();\n state.toolbarContentCleanups.delete(contentId);\n }\n\n // Call onDestroy if defined\n const content = state.toolbarContents.get(contentId);\n if (content?.onDestroy) {\n content.onDestroy();\n }\n\n state.toolbarContents.delete(contentId);\n\n if (initialized) {\n grid.refreshShellHeader?.();\n }\n },\n };\n\n return controller;\n}\n\n/**\n * Update accordion section visual state.\n */\nfunction updateAccordionSectionState(renderRoot: Element, sectionId: string, expanded: boolean): void {\n const section = renderRoot.querySelector(`[data-section=\"${sectionId}\"]`);\n if (section) {\n section.classList.toggle('expanded', expanded);\n }\n}\n\n/**\n * Render content for a single accordion section.\n */\nfunction renderAccordionSectionContent(renderRoot: Element, state: ShellState, sectionId: string): void {\n const panel = state.toolPanels.get(sectionId);\n if (!panel?.render) return;\n\n const contentEl = renderRoot.querySelector(`[data-section=\"${sectionId}\"] .tbw-accordion-content`);\n if (!contentEl) return;\n\n const cleanup = panel.render(contentEl as HTMLElement);\n if (cleanup) {\n state.panelCleanups.set(sectionId, cleanup);\n }\n}\n// #endregion\n","/**\n * Shell Plugin (Class-based)\n *\n * Owns the grid shell: header bar, toolbar content, and tool panels.\n *\n * **Extraction #370 — Phase 1b (ownership inversion):** this plugin now owns\n * the shell **state** for the grid's lifetime (Approach A: state created once,\n * survives plugin re-inits, torn down on disconnect). Light-DOM parsing,\n * per-render work, and DOM construction move here in the following increments;\n * until each lands, core still drives that part via the resolved plugin\n * instance.\n *\n * @module Plugins/Shell\n */\n\nimport { HEADER_CONTENT_DUPLICATE, TOOL_PANEL_DUPLICATE, warnDiagnostic } from '../../core/internal/diagnostics';\nimport { BaseGridPlugin, type GridElement } from '../../core/plugin/base-plugin';\nimport type { GridConfig } from '../../core/types';\nimport {\n cleanupShellState,\n createShellState,\n parseLightDomShell,\n parseLightDomToolButtons,\n parseLightDomToolPanels,\n prepareForRerender,\n rebuildShellDOM,\n renderCustomToolbarContents,\n renderHeaderContent,\n renderPanelContent,\n setupClickOutsideDismiss,\n setupEscapeDismiss,\n setupShellEventListeners,\n setupToolPanelResize,\n shouldRenderShellHeader,\n updatePanelState,\n updateToolbarActiveStates,\n type ShellState,\n} from './shell';\nimport { createShellController, type ShellController, type ShellControllerGrid } from './shell-controller';\nimport styles from './shell.css?inline';\nimport './types';\nimport type {\n HeaderContentDefinition,\n OpenToolPanelOptions,\n ShellConfig,\n ToolbarContentDefinition,\n ToolPanelDefinition,\n} from './types';\n\n/**\n * Shell plugin for `<tbw-grid>`.\n *\n * @see {@link ShellConfig} for all configuration options.\n *\n * @internal Extends BaseGridPlugin.\n * @since 2.x (extraction #370)\n */\nexport class ShellPlugin extends BaseGridPlugin<ShellConfig> {\n /** @internal */\n readonly name = 'shell';\n /** @internal */\n override readonly styles = styles;\n\n // Extraction #370 — Approach A: the plugin owns the shell state for the\n // grid's lifetime. State is created once (via `ensureState`, lazily on first\n // access) and survives plugin re-inits (idempotent attach / non-destructive\n // detach); it is torn down only on grid disconnect via `disposeShellState()`.\n #state?: ShellState;\n #controller?: ShellController;\n\n // Document-level listeners the plugin owns for the shell's interactive\n // affordances. Each `#setupListeners` call cleans the previous registration\n // before re-adding (idempotent across re-wraps); torn down on disconnect.\n #resizeCleanup?: () => void;\n #clickOutsideCleanup?: () => void;\n #escapeDismissCleanup?: () => void;\n\n // The light-DOM shell-change handler (re-parses <tbw-grid-header> /\n // <tbw-grid-tool-buttons> / <tbw-grid-tool-panel> when the framework projects\n // them asynchronously). Registered in `attach` via the generic\n // `_registerLightDomHandler` seam; unregistered on disconnect.\n #shellChangeHandler?: () => void;\n\n // While a dropdown tool panel is open, this observer watches the render root\n // for the trigger being recreated by an outer framework (e.g. a React column\n // `headerRenderer` re-running on a column toggle) and re-pairs the popover's\n // anchor. MutationObserver callbacks run as a pre-paint microtask, so the\n // re-pair lands BEFORE the browser paints the dangling-anchor frame — no\n // flash. It also fires for every toggle (not a coalesced single rAF), so the\n // dropdown no longer intermittently disappears. Connected lazily while a\n // dropdown is open; disconnected on close and on grid disconnect.\n #reanchorObserver?: MutationObserver;\n #reanchorObserving = false;\n\n /** @internal Whether shell state has been created yet. */\n get hasState(): boolean {\n return this.#state !== undefined;\n }\n\n /**\n * @internal Create the canonical shell state + controller (idempotent).\n * The plugin owns shell-state construction; the controller binds to the grid\n * for UI updates. No-op once state exists, so it is safe to call on every\n * `attach` (and from a pre-connect API call), which keeps the shell self-\n * sufficient — core holds no shell-activation knowledge (extraction #370).\n */\n ensureState(grid: GridElement): void {\n if (this.#state) return;\n const state = createShellState();\n this.#state = state;\n this.#controller = createShellController(state, grid as ShellControllerGrid);\n }\n\n /** @internal Canonical shell state. */\n get shellState(): ShellState {\n return this.#state as ShellState;\n }\n\n /** @internal Shell controller. */\n get shellController(): ShellController {\n return this.#controller as ShellController;\n }\n\n // #region Public Shell API\n // The shell plugin's public, programmatic API. Obtain the instance via\n // `grid.getPluginByName('shell')`. These mirror the (now `@deprecated`)\n // `<tbw-grid>` delegate methods, which forward here for the v2.x window and\n // are removed in v3 (extraction #370). Each guards on `hasState` so a call\n // before the shell has initialized is a safe no-op.\n\n /** Whether the tool panel sidebar is currently open. */\n get isToolPanelOpen(): boolean {\n return this.hasState ? this.shellController.isPanelOpen : false;\n }\n\n /** IDs of the currently expanded accordion sections in the tool panel. */\n get expandedToolPanelSections(): string[] {\n return this.hasState ? this.shellController.expandedSections : [];\n }\n\n /**\n * Open the tool panel sidebar.\n * @param panelId - Optional section to expand on open (see {@link ShellController.openToolPanel}).\n * @param options - Optional open options. In `mode: 'dropdown'`, `options.anchor`\n * sets the element the popover anchors to (see {@link OpenToolPanelOptions}).\n */\n openToolPanel(panelId?: string, options?: OpenToolPanelOptions): void {\n this.shellController?.openToolPanel(panelId, options);\n }\n\n /** Close the tool panel sidebar. */\n closeToolPanel(): void {\n this.shellController?.closeToolPanel();\n }\n\n /**\n * Toggle the tool panel sidebar open or closed.\n * @param options - Optional open options forwarded when opening (e.g. `anchor`\n * for dropdown mode; see {@link OpenToolPanelOptions}).\n */\n toggleToolPanel(options?: OpenToolPanelOptions): void {\n this.shellController?.toggleToolPanel(options);\n }\n\n /** Toggle an accordion section expanded or collapsed within the tool panel. */\n toggleToolPanelSection(sectionId: string): void {\n this.shellController?.toggleToolPanelSection(sectionId);\n }\n\n /** Get all registered tool panel definitions. */\n getToolPanels(): ToolPanelDefinition[] {\n return this.hasState ? this.shellController.getToolPanels() : [];\n }\n\n /** Register a custom tool panel section. */\n registerToolPanel(panel: ToolPanelDefinition): void {\n if (!this.hasState) return;\n this.shellState.apiToolPanelIds.add(panel.id);\n this.shellController.registerToolPanel(panel);\n }\n\n /** Unregister a custom tool panel section. */\n unregisterToolPanel(panelId: string): void {\n if (!this.hasState) return;\n this.shellState.apiToolPanelIds.delete(panelId);\n this.shellController.unregisterToolPanel(panelId);\n }\n\n /** Get all registered header content definitions. */\n getHeaderContents(): HeaderContentDefinition[] {\n return this.hasState ? this.shellController.getHeaderContents() : [];\n }\n\n /** Register custom header content (rendered in the shell header bar). */\n registerHeaderContent(content: HeaderContentDefinition): void {\n if (!this.hasState) return;\n this.shellState.apiHeaderContentIds.add(content.id);\n this.shellController.registerHeaderContent(content);\n }\n\n /** Unregister custom header content. */\n unregisterHeaderContent(contentId: string): void {\n if (!this.hasState) return;\n this.shellState.apiHeaderContentIds.delete(contentId);\n this.shellController.unregisterHeaderContent(contentId);\n }\n\n /** Get all registered toolbar content definitions. */\n getToolbarContents(): ToolbarContentDefinition[] {\n return this.hasState ? this.shellController.getToolbarContents() : [];\n }\n\n /** Register custom toolbar content (rendered in the shell toolbar). */\n registerToolbarContent(content: ToolbarContentDefinition): void {\n this.shellController?.registerToolbarContent(content);\n }\n\n /** Unregister custom toolbar content. */\n unregisterToolbarContent(contentId: string): void {\n this.shellController?.unregisterToolbarContent(contentId);\n }\n // #endregion\n\n /** @internal */\n override attach(grid: GridElement): void {\n super.attach(grid);\n // Activate the shell on attach so an opted-in shell renders without core\n // ever calling into it (extraction #370 — core holds no shell-activation\n // knowledge). Idempotent: a pre-connect API call may have created state.\n this.ensureState(grid);\n // Register the light-DOM shell handlers via the generic seam. Core holds no\n // shell tag knowledge (extraction #370). `registerLightDomHandler` is keyed\n // by tag name, so re-attach simply overwrites — idempotent. Torn down on\n // grid disconnect in `disposeShellState` (Approach A: survives re-inits).\n const handler = (): void => this.syncLightDom();\n this.#shellChangeHandler = handler;\n grid._registerLightDomHandler('tbw-grid-header', handler);\n grid._registerLightDomHandler('tbw-grid-tool-buttons', handler);\n grid._registerLightDomHandler('tbw-grid-tool-panel', handler);\n }\n\n /** @internal Parse all light-DOM shell elements into the plugin's state. */\n #parseLightDom(grid: GridElement): void {\n const host = grid._hostElement;\n parseLightDomShell(host, this.shellState);\n parseLightDomToolButtons(host, this.shellState);\n parseLightDomToolPanels(host, this.shellState, grid._getToolPanelRendererFactory());\n }\n\n /**\n * @internal Generic hook: react to a light-DOM shell element being\n * added/removed/mutated (e.g. a framework projecting `<tbw-grid-header title>`\n * asynchronously). Re-parses, and if a title or tool-buttons container newly\n * appeared, re-merges the config (so `processConfig` folds it in) and re-wraps\n * the already-rendered shell header. No-op until the shell header is rendered.\n * Called by core from `refreshColumns` and the light-DOM observer.\n */\n override syncLightDom(): void {\n const grid = this.grid;\n if (!grid || !this.hasState) return;\n const state = this.shellState;\n const hadTitle = state.lightDomTitle;\n const hadToolButtons = state.hasToolButtonsContainer;\n this.#parseLightDom(grid);\n const hasTitle = state.lightDomTitle;\n const hasToolButtons = state.hasToolButtonsContainer;\n\n if ((hasTitle && !hadTitle) || (hasToolButtons && !hadToolButtons)) {\n grid._requestConfigMerge();\n // Re-wrap the shell header to reflect the new title / tool buttons.\n // `afterStructuralRender` rebuilds the chrome AND re-renders all shell\n // contents (header content, toolbar slots, panel), so no manual render is\n // needed here.\n if (grid._renderRoot.querySelector('.tbw-shell-header')) {\n prepareForRerender(state);\n this.afterStructuralRender();\n }\n }\n }\n\n // #region Generic structural hooks (extraction #370)\n // Core calls these via the plugin manager (parseLightDom /\n // beforeStructuralRender / needsStructuralRerender) — it holds no\n // shell-specific orchestration. The shell self-parses, self-prepares, and\n // self-decides re-render needs from its own state + DOM signature.\n\n /** @internal Generic hook: re-parse all light-DOM shell elements into state. */\n override parseLightDom(): void {\n const grid = this.grid;\n if (!grid || !this.hasState) return;\n this.#parseLightDom(grid);\n }\n\n /** @internal Generic hook: move toolbar buttons back ahead of a structural rebuild. */\n override beforeStructuralRender(): void {\n if (!this.hasState) return;\n prepareForRerender(this.shellState);\n }\n\n /**\n * @internal Generic hook: whether the shell's rendered DOM signature no longer\n * matches the new effective config (header appearing/disappearing, tool-panel\n * count/position/mode change) and therefore needs a structural re-render.\n * Reads the still-current (old) DOM vs the freshly merged config.\n */\n override needsStructuralRerender(): boolean {\n const grid = this.grid;\n if (!grid || !this.hasState) return false;\n const root = grid._renderRoot;\n const hadShell = !!root.querySelector('.has-shell');\n const hadToolPanel = !!root.querySelector('.tbw-tool-panel');\n const accordionSectionsBefore = root.querySelectorAll('.tbw-accordion-section').length;\n const prevPosition = (root.querySelector('.tbw-tool-panel') as HTMLElement | null)?.dataset.position ?? 'right';\n const prevMode = (root.querySelector('.tbw-shell-body') as HTMLElement | null)?.dataset.mode ?? 'overlay';\n\n const shell = grid.effectiveConfig?.shell;\n const nowNeedsShell = shouldRenderShellHeader(shell);\n const nowHasToolPanels = (shell?.toolPanels?.length ?? 0) > 0;\n const toolPanelCount = shell?.toolPanels?.length ?? 0;\n const newPosition = shell?.toolPanel?.position ?? 'right';\n const newMode = shell?.toolPanel?.mode ?? 'overlay';\n\n return (\n hadShell !== nowNeedsShell ||\n (!hadToolPanel && nowHasToolPanels) ||\n (hadToolPanel && toolPanelCount !== accordionSectionsBefore) ||\n (hadToolPanel && prevPosition !== newPosition) ||\n (hadToolPanel && prevMode !== newMode)\n );\n }\n // #endregion\n\n /**\n * @internal Fold the plugin's shell state into the effective config.\n *\n * Called by core's config merge ({@link ConfigManager.merge}) via the neutral\n * `processConfig` plugin hook \\u2014 core holds no shell-config knowledge. Mutates\n * `config.shell` in place from light-DOM-parsed + API-registered shell state\n * (title, header content, tool buttons, tool panels, header/toolbar contents).\n *\n * Extraction #370, Task 1b \\u2014 config ownership inversion: replaces core's old\n * `ConfigManager.#mergeShellConfig`.\n */\n /**\n * @internal Self-collect plugin tool panels + header contents into shell\n * state. Clears stale plugin-contributed entries (preserving light-DOM +\n * API-registered) then re-collects fresh from all attached plugins via the\n * neutral `grid._pluginShellContributions()` seam. Idempotent — safe to run on\n * every config merge, which is how plugin re-inits are picked up.\n *\n * Extraction #370: replaces core's `#collectPluginShellContributions` plus the\n * plugin-reinit cleanup block in core's `#updatePluginConfigs`.\n */\n #syncPluginContributions(grid: GridElement): void {\n const state = this.shellState;\n const gridId = grid._hostElement.id;\n\n // Drop stale plugin-contributed tool panels (keep light-DOM + API panels).\n for (const panelId of [...state.toolPanels.keys()]) {\n if (state.lightDomToolPanelIds.has(panelId) || state.apiToolPanelIds.has(panelId)) continue;\n state.panelCleanups.get(panelId)?.();\n state.panelCleanups.delete(panelId);\n state.toolPanels.delete(panelId);\n }\n // Drop stale plugin-contributed header contents (keep API-registered).\n for (const contentId of [...state.headerContents.keys()]) {\n if (state.apiHeaderContentIds.has(contentId)) continue;\n state.headerContentCleanups.get(contentId)?.();\n state.headerContentCleanups.delete(contentId);\n state.headerContents.delete(contentId);\n }\n\n const { toolPanels, headerContents } = grid._pluginShellContributions();\n\n for (const panel of toolPanels) {\n // Precedence (#370): an API-registered panel wins over a plugin; a plugin\n // wins over a colliding light-DOM panel (the plugin owns its own DOM, so\n // it must own the id).\n if (state.apiToolPanelIds.has(panel.id)) continue;\n if (state.lightDomToolPanelIds.has(panel.id)) {\n warnDiagnostic(\n TOOL_PANEL_DUPLICATE,\n `Tool panel \"${panel.id}\" is provided by a plugin; ignoring the matching light-DOM <tbw-grid-tool-panel>.`,\n gridId,\n );\n state.panelCleanups.get(panel.id)?.();\n state.panelCleanups.delete(panel.id);\n state.lightDomToolPanelIds.delete(panel.id);\n state.adapterBoundToolPanelIds.delete(panel.id);\n }\n state.toolPanels.set(panel.id, panel);\n }\n\n for (const content of headerContents) {\n if (state.apiHeaderContentIds.has(content.id)) continue;\n if (state.headerContents.has(content.id)) {\n warnDiagnostic(\n HEADER_CONTENT_DUPLICATE,\n `Header content \"${content.id}\" is provided by a plugin; ignoring the matching light-DOM definition.`,\n gridId,\n );\n state.headerContentCleanups.get(content.id)?.();\n state.headerContentCleanups.delete(content.id);\n }\n state.headerContents.set(content.id, content);\n }\n }\n\n override processConfig(config: GridConfig): void {\n if (!this.hasState) return;\n const state = this.shellState;\n\n // Self-parse light-DOM shell elements + self-collect plugin shell\n // contributions before folding state into config (extraction #370 —\n // replaces core's `#parseLightDom` + `#collectPluginShellContributions`).\n const grid = this.grid;\n if (grid) {\n this.#parseLightDom(grid);\n this.#syncPluginContributions(grid);\n }\n\n // Constructor-supplied shell config is the base layer: the canonical\n // `features: { shell: ... }` opt-in passes the config value here (and an\n // explicit `plugins: [new ShellPlugin(cfg)]` does too). An explicit\n // top-level `gridConfig.shell` overrides it; parsed light-DOM / API state\n // folds on top below. Empty for the auto-registered instance\n // (`new ShellPlugin()`), so existing default-on behavior is unchanged.\n const ctor = this.resolvedConfig;\n\n // Clone shell hierarchy to avoid mutating the original gridConfig.\n config.shell = { ...ctor, ...config.shell };\n config.shell.header = { ...ctor.header, ...config.shell.header };\n\n // Light DOM title (does not override an explicit config title).\n if (state.lightDomTitle && !config.shell.header.title) {\n config.shell.header.title = state.lightDomTitle;\n }\n\n // Light DOM header content elements.\n if (state.lightDomHeaderContent?.length > 0) {\n config.shell.header.lightDomContent = state.lightDomHeaderContent;\n }\n\n // Tool-buttons container presence.\n if (state.hasToolButtonsContainer) {\n config.shell.header.hasToolButtonsContainer = true;\n }\n\n // Tool panels (from plugins + API + Light DOM), sorted by order.\n if (state.toolPanels.size > 0) {\n const panels = Array.from(state.toolPanels.values());\n panels.sort((a, b) => (a.order ?? 100) - (b.order ?? 100));\n config.shell.toolPanels = panels;\n }\n\n // Header contents (from plugins + API), sorted by order.\n if (state.headerContents.size > 0) {\n const contents = Array.from(state.headerContents.values());\n contents.sort((a, b) => (a.order ?? 100) - (b.order ?? 100));\n config.shell.headerContents = contents;\n }\n\n // Toolbar contents: merge ORIGINAL config contents (from the source\n // gridConfig, not a previous merge) with API-registered contents so stale\n // API entries never accumulate. Config takes precedence by id.\n const apiContents = Array.from(state.toolbarContents.values());\n const originalConfigContents = this.grid?._sourceConfig?.shell?.header?.toolbarContents ?? [];\n const configIds = new Set(originalConfigContents.map((c) => c.id));\n const mergedContents = [...originalConfigContents];\n for (const content of apiContents) {\n if (!configIds.has(content.id)) {\n mergedContents.push(content);\n }\n }\n mergedContents.sort((a, b) => (a.order ?? 0) - (b.order ?? 0));\n config.shell.header.toolbarContents = mergedContents;\n }\n\n /**\n * @internal Wrap the freshly built (bare) grid DOM in shell chrome.\n *\n * Fired synchronously by core at the end of every structural (re)build, before\n * paint. Core builds a BARE grid; this hook relocates the existing\n * `.tbw-grid-content` node into shell chrome (header bar, toolbar, tool panel)\n * via {@link rebuildShellDOM}, which preserves the content subtree (and all its\n * listeners / cached refs) intact. No-op when no shell is configured.\n *\n * Extraction #370, Task 1b — DOM ownership inversion: the shell DOM is now\n * constructed by the plugin, not core.\n */\n override afterStructuralRender(): void {\n const grid = this.grid;\n if (!grid || !this.hasState) return;\n const state = this.shellState;\n const hasShell = rebuildShellDOM(\n grid._renderRoot,\n grid.effectiveConfig?.shell,\n { isPanelOpen: state.isPanelOpen, expandedSections: state.expandedSections },\n grid.effectiveConfig?.icons,\n );\n if (hasShell) {\n this.#setupListeners(grid);\n this.shellController.setInitialized(true);\n this.#renderShellContents(grid);\n }\n }\n\n /**\n * @internal Re-anchor an open dropdown tool panel after a NON-structural\n * render.\n *\n * A column-visibility toggle (or reorder) re-renders the grid header WITHOUT\n * a full structural rebuild — it fires `afterRender`, not\n * `afterStructuralRender`. When the column-chooser trigger lives in the grid\n * header (e.g. a custom `[data-panel-toggle]` button in a column\n * `headerRenderer`), that re-render recreates the trigger node, so it loses\n * the `anchor-name` that paired it with the open popover. The popover then\n * collapses to the viewport / grid corner.\n *\n * Two cooperating mechanisms keep the popover anchored with NO flash:\n * 1. A synchronous {@link ShellController.reanchorOpenDropdown} here catches\n * the case where the trigger was recreated synchronously during this\n * render (in-place re-pair, before paint).\n * 2. A {@link MutationObserver} (started here, kept running while the panel is\n * open) catches the case where an outer framework commits the replacement\n * trigger on a LATER task. Its callback runs as a pre-paint microtask, so\n * the re-pair lands before the dangling-anchor frame is painted.\n *\n * `reanchorOpenDropdown()` is idempotent and cheap (early-returns unless a\n * dropdown is open with a broken anchor), so running it on every render and\n * every mutation is safe.\n */\n override afterRender(): void {\n const grid = this.grid;\n if (!grid || !this.hasState) return;\n const state = this.shellState;\n\n // In-place shell header title sync (extraction #370 — replaces core's\n // `#updateShellHeaderInPlace`). Idempotent: only touches the existing\n // `.tbw-shell-header`, no-op when no shell is rendered.\n this.#syncShellHeaderTitle(grid);\n\n // Re-render any open tool-panel sections whose content was cleared during a\n // plugin re-init (extraction #370 — replaces core's `_renderOpenPanelContent`).\n // `renderPanelContent` is idempotent — it only fills empty sections.\n if (state.isPanelOpen) {\n renderPanelContent(grid._renderRoot, state);\n }\n\n // Dropdown re-anchor after a NON-structural render (see method docs below).\n if (!state.isPanelOpen) {\n this.#stopReanchorObserver();\n return;\n }\n this.shellController.reanchorOpenDropdown();\n this.#startReanchorObserver(grid);\n }\n\n /**\n * @internal Sync the shell header title element in place from the effective\n * config (or parsed light-DOM title) without a full re-render. Extracted from\n * core's former `#updateShellHeaderInPlace` (#370).\n */\n #syncShellHeaderTitle(grid: GridElement): void {\n const shellHeader = grid._renderRoot.querySelector('.tbw-shell-header');\n if (!shellHeader) return;\n\n const title = grid.effectiveConfig?.shell?.header?.title ?? this.shellState.lightDomTitle;\n\n let titleEl = shellHeader.querySelector('.tbw-shell-title') as HTMLElement | null;\n if (title) {\n if (!titleEl) {\n titleEl = document.createElement('h2');\n titleEl.className = 'tbw-shell-title';\n titleEl.setAttribute('part', 'shell-title');\n shellHeader.insertBefore(titleEl, shellHeader.firstChild);\n }\n titleEl.textContent = title;\n } else if (titleEl) {\n titleEl.remove();\n }\n }\n\n /** @internal Start (idempotently) the dropdown re-anchor observer. */\n #startReanchorObserver(grid: GridElement): void {\n // Only dropdown mode needs the observer. If the mode changed away from\n // 'dropdown' while the panel is still open, tear down any observer started\n // by an earlier render instead of leaving it watching the render root.\n if (grid.effectiveConfig?.shell?.toolPanel?.mode !== 'dropdown') {\n this.#stopReanchorObserver();\n return;\n }\n if (typeof MutationObserver === 'undefined') return;\n if (!this.#reanchorObserver) {\n this.#reanchorObserver = new MutationObserver(() => {\n if (!this.grid || !this.hasState || !this.shellState.isPanelOpen) {\n this.#stopReanchorObserver();\n return;\n }\n this.shellController.reanchorOpenDropdown();\n });\n }\n if (!this.#reanchorObserving) {\n this.#reanchorObserver.observe(grid._renderRoot, { childList: true, subtree: true });\n this.#reanchorObserving = true;\n }\n }\n\n /** @internal Stop the dropdown re-anchor observer if running. */\n #stopReanchorObserver(): void {\n if (this.#reanchorObserver && this.#reanchorObserving) {\n this.#reanchorObserver.disconnect();\n this.#reanchorObserving = false;\n }\n }\n\n /**\n * @internal Render all shell contents into the freshly built shell chrome.\n *\n * Runs from {@link afterStructuralRender} after the chrome is wrapped and\n * listeners are attached. Renders plugin header content + custom toolbar\n * slots, applies the configured default-open / legacy open-on-load behavior,\n * and restores an already-open panel's content. All render functions operate\n * on the shell DOM (queried from `_renderRoot`), not on the grid-body refs,\n * so they are safe to run before `#afterConnect` caches those.\n *\n * Extraction #370, Task 1b (Step 1b.1.3) — replaces core's per-render shell\n * content block in `#afterConnect` / `#afterShellRefresh`.\n */\n #renderShellContents(grid: GridElement): void {\n const state = this.shellState;\n const renderRoot = grid._renderRoot;\n const shellConfig = grid.effectiveConfig?.shell;\n\n // Render plugin header content + custom toolbar contents (render modes).\n renderHeaderContent(renderRoot, state);\n renderCustomToolbarContents(renderRoot, shellConfig, state);\n\n // Pre-expand the configured default section.\n const toolPanelCfg = shellConfig?.toolPanel;\n const defaultOpen = toolPanelCfg?.defaultOpen;\n if (defaultOpen && state.toolPanels.has(defaultOpen)) {\n state.expandedSections.add(defaultOpen);\n }\n\n // Decide whether the sidebar should be open on load.\n // NOTE: In v2.x `defaultOpen` alone also opens the sidebar (legacy\n // behavior). v3.0.0 will drop that — see issue #259 and the\n // @deprecated note on ToolPanelConfig.defaultOpen.\n // Search marker: TOOLPANEL-OPEN-LEGACY-259\n const shouldOpenOnLoad =\n toolPanelCfg?.locked === true ||\n toolPanelCfg?.initialState === 'open' ||\n // Legacy v2 path — remove in v3.0.0 (#259)\n (toolPanelCfg?.initialState === undefined && defaultOpen !== undefined && state.toolPanels.has(defaultOpen));\n if (!state.isPanelOpen && state.toolPanels.size > 0 && shouldOpenOnLoad) {\n this.openToolPanel();\n }\n\n // Restore panel content if the panel was already open (e.g. after a position\n // change re-render or a plugin re-init that triggered a shell refresh).\n if (state.isPanelOpen) {\n updatePanelState(renderRoot, state);\n renderPanelContent(renderRoot, state, {\n expand: grid.effectiveConfig?.icons?.expand,\n collapse: grid.effectiveConfig?.icons?.collapse,\n });\n updateToolbarActiveStates(renderRoot, state);\n // In dropdown mode `rebuildShellDOM` created a fresh `.tbw-tool-panel`, so\n // the popover must be re-shown in the top layer and re-anchored — the\n // trigger may have been recreated by the same re-render (e.g. a custom\n // column-header button after a column toggle/reorder). Otherwise the\n // popover loses its anchor and collapses to the viewport corner.\n this.shellController.reanchorOpenDropdown();\n }\n }\n\n /**\n * @internal Attach shell event + dismiss/resize listeners.\n *\n * Cleans any prior document-level registrations before re-adding, so it is\n * safe to call on every re-wrap (initial render, shell refresh, plugin\n * re-init) without leaking or double-attaching listeners.\n */\n #setupListeners(grid: GridElement): void {\n const renderRoot = grid._renderRoot;\n const shellConfig = grid.effectiveConfig?.shell;\n const state = this.shellState;\n\n setupShellEventListeners(renderRoot, shellConfig, state, {\n onPanelToggle: () => this.toggleToolPanel(),\n onSectionToggle: (sectionId: string) => this.toggleToolPanelSection(sectionId),\n onPanelClose: () => this.closeToolPanel(),\n });\n\n // Tool panel resize handle — persists the new width as a CSS variable on\n // the host element.\n this.#resizeCleanup?.();\n this.#resizeCleanup = setupToolPanelResize(renderRoot, shellConfig, (width: number) => {\n grid._hostElement.style.setProperty('--tbw-tool-panel-width', `${width}px`);\n });\n\n // Click-outside dismiss for an open overlay tool panel.\n this.#clickOutsideCleanup?.();\n this.#clickOutsideCleanup = setupClickOutsideDismiss(grid._hostElement, shellConfig, state, () =>\n this.closeToolPanel(),\n );\n\n // Escape-to-close dismiss for overlay tool panels.\n this.#escapeDismissCleanup?.();\n this.#escapeDismissCleanup = setupEscapeDismiss(shellConfig, state, () => this.closeToolPanel());\n }\n\n /** @internal Remove the document-level shell listeners. */\n #teardownListeners(): void {\n this.#resizeCleanup?.();\n this.#resizeCleanup = undefined;\n this.#clickOutsideCleanup?.();\n this.#clickOutsideCleanup = undefined;\n this.#escapeDismissCleanup?.();\n this.#escapeDismissCleanup = undefined;\n }\n\n /** @internal */\n override detach(): void {\n // Non-destructive detach (Approach A): shell state and document listeners\n // survive plugin re-inits. `#setupListeners` cleans-before-add on the next\n // re-wrap, so there is nothing to tear down here. Real teardown happens in\n // `disposeShellState()` on grid disconnect.\n super.detach();\n }\n\n /**\n * @internal Tear down shell state on grid disconnect (Approach A).\n * Idempotent and harmless when no state exists.\n */\n disposeShellState(): void {\n this.#teardownListeners();\n this.#stopReanchorObserver();\n this.#reanchorObserver = undefined;\n // Unregister the light-DOM shell handlers (registered in `attach`). `detach`\n // does not clear `this.grid`, so the reference is still valid here.\n const grid = this.grid as GridElement | undefined;\n if (grid && this.#shellChangeHandler) {\n grid._unregisterLightDomHandler('tbw-grid-header');\n grid._unregisterLightDomHandler('tbw-grid-tool-buttons');\n grid._unregisterLightDomHandler('tbw-grid-tool-panel');\n this.#shellChangeHandler = undefined;\n }\n if (this.#state) {\n cleanupShellState(this.#state);\n this.#controller?.setInitialized(false);\n }\n }\n}\n"],"names":["gridPrefix","gridId","pluginName","TOOL_PANEL_NOT_FOUND","TOOL_PANEL_DUPLICATE","HEADER_CONTENT_DUPLICATE","formatDiagnostic","code","message","toLowerCase","docsUrl","warnDiagnostic","console","warn","_F","split","map","s","reverse","join","RegExp","slice","DANGEROUS_TAGS","Set","DANGEROUS_ATTR_PATTERN","URL_ATTRS","DANGEROUS_URL_PROTOCOL","sanitizeHTML","html","indexOf","template","document","createElement","innerHTML","root","toRemove","elements","querySelectorAll","el","tagName","has","push","namespaceURI","Array","from","attributes","some","attr","test","name","attrsToRemove","attrName","value","forEach","removeAttribute","remove","sanitizeNode","content","DEFAULT_GRID_ICONS","expand","collapse","sortAsc","sortDesc","sortNone","submenuArrow","dragHandle","toolPanel","filter","filterActive","print","BaseGridPlugin","static","aliases","version","styles","cellRenderers","headerRenderers","cellEditors","grid","config","userConfig","abortController","defaultConfig","constructor","this","resolvedConfig","mergeConfigsFrom","others","length","merged","sources","key","Object","keys","other","otherConfig","entries","ownerA","ownerB","Error","assign","refreshUserConfigFrom","target","source","attach","abort","AbortController","detach","getPlugin","PluginClass","emit","eventName","detail","dispatchEvent","CustomEvent","bubbles","emitCancelable","event","cancelable","defaultPrevented","on","eventType","callback","_pluginManager","subscribe","off","unsubscribe","emitPluginEvent","broadcast","requestRender","requestColumnsRender","requestRenderWithFocus","requestAfterRender","requestVirtualRefresh","rows","sourceRows","columns","visibleColumns","_visibleColumns","gridElement","_hostElement","disconnectSignal","signal","gridIcons","userIcons","gridConfig","icons","isAnimationEnabled","mode","effectiveConfig","animation","host","getComputedStyle","getPropertyValue","trim","animationDuration","durationStr","parsed","parseInt","isNaN","setIcon","element","iconKey","pluginOverride","dataset","icon","replace","expanded","jsIcon","resolveJsIcon","HTMLElement","appendChild","cloneNode","updateSortIndicator","cell","direction","querySelector","indicator","setAttribute","className","insertBefore","codeOrMessage","id","throwDiagnostic","GridClasses","applyAttrs","attrs","tag","children","div","button","type","gridContentTemplate","cloneGridContent","buildGridDOM","options","fragment","createDocumentFragment","hasShell","shellBody","shellHeader","contentWrapper","buildShellHeader","header","part","role","title","titleEl","textContent","toolbar","btn","configButtons","hasRender","apiButtons","hasCustomContent","b","showToggle","hasPanels","toggleBtn","isPanelOpen","String","toolPanelIcon","buildShellBody","body","hasPanel","panels","isSinglePanel","gridContent","panelEl","class","position","popover","resizeHandlePosition","makeCloseButton","inline","closeBtn","inlineClose","showCloseButton","panelHeader","panelContent","accordion","panel","section","isExpanded","headerBtn","iconSpan","titleSpan","chevronSpan","expandIcon","headerRow","iconToString","outerHTML","shouldRenderShellHeader","toolbarContents","toolPanels","headerContents","lightDomContent","hasToolButtonsContainer","shouldRenderHeaderBar","visible","isInGrid","node","contains","getRootNode","ShadowRoot","renderHeaderContent","renderRoot","state","hasLightDomContent","lightDomHeaderContent","lightDomContentMoved","hasPluginContent","size","contentArea","style","display","sortedContents","values","sort","a","order","existingCleanup","headerContentCleanups","get","delete","container","cleanup","render","set","renderPanelContent","_icons","panelId","expandedSections","classList","toggle","panelCleanups","updateToolbarActiveStates","panelToggle","updatePanelState","width","DROPDOWN_ANCHOR_NAME","dropdownAnchorSeq","supportsAnchorPositioning","CSS","supports","showToolPanelDropdown","anchorEl","corner","dropdownAnchorEl","anchor","hasAttribute","showPopover","anchorName","dropdownAnchorName","setProperty","removeProperty","rect","getBoundingClientRect","getAttribute","top","Math","round","bottom","left","right","viewportWidth","window","innerWidth","positionDropdownFallback","prepareForRerender","toolbarContentCleanups","clear","buildShellOptions","shellConfig","runtimeState","collapseIcon","sortedPanels","headerOptions","toolPanelToggle","locked","c","hasElement","bodyOptions","p","rebuildShellDOM","existingRoot","existingContent","preservedNodes","n","firstChild","nextSibling","nodeType","replaceChildren","buildGridDOMIntoElement","freshContent","replaceWith","createShellController","initialized","controller","isInitialized","setInitialized","openToolPanel","targetSectionId","toggleToolPanelSection","add","defaultOpenId","shell","defaultOpen","firstPanel","shadow","_renderRoot","_accordionIcons","explicit","resolveDropdownAnchor","_emit","sections","closeToolPanel","onClose","hidePopover","hideToolPanelDropdown","toggleToolPanel","reanchorOpenDropdown","stored","isConnected","fresh","trigger","prev","repairDropdownAnchor","sectionId","updateAccordionSectionState","otherId","otherPanel","contentEl","renderAccordionSectionContent","getToolPanels","registerToolPanel","refreshShellHeader","unregisterToolPanel","getHeaderContents","registerHeaderContent","unregisterHeaderContent","contentId","onDestroy","getToolbarContents","registerToolbarContent","unregisterToolbarContent","ShellPlugin","resizeCleanup","clickOutsideCleanup","escapeDismissCleanup","shellChangeHandler","reanchorObserver","reanchorObserving","hasState","ensureState","Map","lightDomTitle","lightDomToolPanelIds","adapterBoundToolPanelIds","lightDomToolbarContentIds","apiToolPanelIds","apiHeaderContentIds","shellState","shellController","isToolPanelOpen","expandedToolPanelSections","super","handler","syncLightDom","_registerLightDomHandler","parseLightDom","headerEl","parseLightDomShell","toolButtonsContainer","contentDef","parseLightDomToolButtons","rendererFactory","tooltip","adapterRenderer","wrapper","existingPanel","firstAdapterAttach","attrsChanged","parseLightDomToolPanels","_getToolPanelRendererFactory","hadTitle","hadToolButtons","hasTitle","hasToolButtons","_requestConfigMerge","afterStructuralRender","beforeStructuralRender","needsStructuralRerender","hadShell","hadToolPanel","accordionSectionsBefore","prevPosition","prevMode","syncPluginContributions","_pluginShellContributions","processConfig","ctor","contents","apiContents","originalConfigContents","_sourceConfig","configIds","mergedContents","setupListeners","renderShellContents","afterRender","syncShellHeaderTitle","startReanchorObserver","stopReanchorObserver","MutationObserver","observe","childList","subtree","disconnect","configContents","stateContents","allContents","slot","renderCustomToolbarContents","toolPanelCfg","shouldOpenOnLoad","initialState","callbacks","addEventListener","e","closest","onPanelToggle","onPanelClose","onSectionToggle","setupShellEventListeners","onResize","handle","startX","startWidth","maxWidth","isResizing","onMouseMove","preventDefault","delta","clientX","newWidth","min","max","onMouseUp","transition","cursor","userSelect","finalWidth","removeEventListener","onMouseDown","setupToolPanelResize","closeOnClickOutside","composedPath","Element","matches","setupClickOutsideDismiss","setupEscapeDismiss","teardownListeners","disposeShellState","_unregisterLightDomHandler","cleanupShellState"],"mappings":"AA+BO,SAASA,EAAWC,EAAiBC,GAG1C,MAAO,YAFID,EAAS,IAAIA,IAAW,KACpBC,EAAa,IAAIA,IAAe,KAEjD,CAoDO,MAsCMC,EAAuB,SAEvBC,EAAuB,SAEvBC,EAA2B,SA8HjC,SAASC,EAAiBC,EAAsBC,EAAiBP,EAAiBC,GAEvF,MAAO,GADQF,EAAWC,EAAQC,MACdK,MAASC,uBApB/B,SAAiBD,GACf,MAAO,qCAAgBA,EAAKE,eAC9B,CAkB4DC,CAAQH,IACpE,CAkBO,SAASI,EAAeJ,EAAsBC,EAAiBP,EAAiBC,GACrFU,QAAQC,KAAKP,EAAiBC,EAAMC,EAASP,EAAQC,GACvD,CCzQA,MAAMY,EACJ,4SACGC,MAAM,KACNC,IAAKC,GAAMA,EAAEF,MAAM,IAAIG,UAAUC,KAAK,KACzB,IAAIC,OAAO,uCAAuCN,EAAGO,MAAM,GAAGF,KAAK,gBA0BrF,MAAMG,EAAiB,IAAIC,IACzB,sKAAsKR,MACpK,MAOES,EAAyB,WAKzBC,EAAY,IAAIF,IAAI,kEAAkER,MAAM,MAK5FW,EAAyB,wCAkBxB,SAASC,EAAaC,GAC3B,IAAKA,GAAwB,iBAATA,EAAmB,MAAO,GAG9C,QAAIA,EAAKC,QAAQ,KAAa,OAAOD,EAErC,MAAME,EAAWC,SAASC,cAAc,YAKxC,OAJAF,EAASG,UAAYL,EAUvB,SAAsBM,GACpB,MAAMC,EAAsB,GAGtBC,EAAWF,EAAKG,iBAAiB,KAEvC,IAAA,MAAWC,KAAMF,EAAU,CACzB,MAAMG,EAAUD,EAAGC,QAAQ9B,cAG3B,GAAIa,EAAekB,IAAID,GAAU,CAC/BJ,EAASM,KAAKH,GACd,QACF,CAGA,GAAgB,QAAZC,GAAyC,+BAApBD,EAAGI,aAA+C,CAKzE,GAH4BC,MAAMC,KAAKN,EAAGO,YAAYC,KACnDC,GAASvB,EAAuBwB,KAAKD,EAAKE,OAAuB,SAAdF,EAAKE,MAAiC,eAAdF,EAAKE,MAE1D,CACvBd,EAASM,KAAKH,GACd,QACF,CACF,CAGA,MAAMY,EAA0B,GAChC,IAAA,MAAWH,KAAQT,EAAGO,WAAY,CAChC,MAAMM,EAAWJ,EAAKE,KAAKxC,cAGvBe,EAAuBwB,KAAKG,GAC9BD,EAAcT,KAAKM,EAAKE,OAKtBxB,EAAUe,IAAIW,IAAazB,EAAuBsB,KAAKD,EAAKK,QAM/C,UAAbD,GAAwB,4CAA4CH,KAAKD,EAAKK,SALhFF,EAAcT,KAAKM,EAAKE,KAS5B,CAEAC,EAAcG,QAASJ,GAASX,EAAGgB,gBAAgBL,GACrD,CAGAd,EAASkB,QAASf,GAAOA,EAAGiB,SAC9B,CAhEEC,CAAa1B,EAAS2B,SAEf3B,EAASG,SAClB,CCyhHO,MAAMyB,EAA0C,CACrDC,OAAQ,IACRC,SAAU,IACVC,QAAS,IACTC,SAAU,IACVC,SAAU,IACVC,aAAc,IACdC,WAAY,KACZC,UAAW,IACXC,OAAQ,GACRC,aAAc,GACdC,MAAO,OCzsGF,MAAeC,EAiBpBC,oBAuBAA,gBAUSC,QAMAC,QAA4D,eAG5DC,OAGAC,cAGAC,gBAGAC,YAGCC,KAGAC,OAGSC,WAOnBC,GAOA,iBAAcC,GACZ,MAAO,CAAA,CACT,CAEA,WAAAC,CAAYJ,EAA2B,IACrCK,KAAKJ,WAAaD,CACpB,CAgBA,kBAAIM,GACF,OAAQD,MAAKH,EAAmBG,KAAKL,OAAS,IAAKK,KAAKF,iBAAkBE,KAAKJ,WACjF,CAwBA,gBAAAM,CAAiBC,GACf,GAAsB,IAAlBA,EAAOC,OAAc,OACzB,MAAMC,EAAS,IAAKL,KAAKJ,YACnBU,EAAmD,CAAA,EACzD,IAAA,MAAWC,KAAOC,OAAOC,KAAKJ,GAASC,EAAQC,GAAOP,KACtD,IAAA,MAAWU,KAASP,EAAQ,CAC1B,MAAMQ,EAAcD,EAAMd,WAC1B,IAAA,MAAYW,EAAKvC,KAAUwC,OAAOI,QAAQD,GAAc,CACtD,QAAc,IAAV3C,EAAqB,SACzB,KAAMuC,KAAOF,GAAS,CACpBA,EAAOE,GAAOvC,EACdsC,EAAQC,GAAOG,EACf,QACF,CACA,GAAIL,EAAOE,KAASvC,EAAO,SAE3B,MAAM6C,EAASP,EAAQC,IAAMR,YAAYlC,MAAQmC,KAAKD,YAAYlC,KAC5DiD,EAASJ,EAAMX,YAAYlC,KAC3B1C,EAAOD,EH7fuB,SG+flC,oCAAoC8E,KAAKnC,iCAAiC0C,uBAAyBM,SAAcC,yEACjH,EACAd,KAAKnC,MAEP,MAAM,IAAIkD,MAAM5F,EAClB,CACF,CAMAqF,OAAOQ,OAAOhB,KAAKJ,WAAuCS,EAC5D,CAiBA,qBAAAY,CAAsBP,GACpB,MAAMQ,EAASlB,KAAKJ,WASduB,EAAS,IAAMT,EAAMd,YAC3B,IAAA,MAAWW,KAAOC,OAAOC,KAAKS,UAAgBA,EAAOX,GACrDC,OAAOQ,OAAOE,EAAQC,EACxB,CAiBA,MAAAC,CAAO1B,GAELM,MAAKH,GAAkBwB,QAEvBrB,MAAKH,EAAmB,IAAIyB,gBAE5BtB,KAAKN,KAAOA,EAEZM,KAAKL,OAAS,IAAKK,KAAKF,iBAAkBE,KAAKJ,WACjD,CAeA,MAAA2B,GAGEvB,MAAKH,GAAkBwB,QACvBrB,MAAKH,OAAmB,CAE1B,CAwDU,SAAA2B,CAAoCC,GAC5C,OAAOzB,KAAKN,MAAM8B,UAAUC,EAC9B,CAKU,IAAAC,CAAQC,EAAmBC,GACnC5B,KAAKN,MAAMmC,gBAAgB,IAAIC,YAAYH,EAAW,CAAEC,SAAQG,SAAS,IAC3E,CAMU,cAAAC,CAAkBL,EAAmBC,GAC7C,MAAMK,EAAQ,IAAIH,YAAYH,EAAW,CAAEC,SAAQG,SAAS,EAAMG,YAAY,IAE9E,OADAlC,KAAKN,MAAMmC,gBAAgBI,GACpBA,EAAME,gBACf,CAsBU,EAAAC,CAAgBC,EAAmBC,GAC3CtC,KAAKN,MAAM6C,gBAAgBC,UAAUxC,KAAMqC,EAAWC,EACxD,CAaU,GAAAG,CAAIJ,GACZrC,KAAKN,MAAM6C,gBAAgBG,YAAY1C,KAAMqC,EAC/C,CAoBU,eAAAM,CAAmBN,EAAmBT,GAC9C5B,KAAKN,MAAM6C,gBAAgBI,gBAAgBN,EAAWT,EACxD,CAoBU,SAAAgB,CAAaP,EAAmBT,GACxC5B,KAAK2C,gBAAgBN,EAAWT,GAChC5B,KAAK0B,KAAKW,EAAWT,EACvB,CAMU,aAAAiB,GACR7C,KAAKN,MAAMmD,iBACb,CAOU,oBAAAC,GACP9C,KAAKN,MAAgDoD,wBACxD,CAOU,sBAAAC,GACR/C,KAAKN,MAAMqD,0BACb,CAMU,kBAAAC,GACRhD,KAAKN,MAAMsD,sBACb,CAOU,qBAAAC,GACRjD,KAAKN,MAAMuD,yBACb,CAKA,QAAcC,GACZ,OAAOlD,KAAKN,MAAMwD,MAAQ,EAC5B,CAMA,cAAcC,GACZ,OAAOnD,KAAKN,MAAMyD,YAAc,EAClC,CAKA,WAAcC,GACZ,OAAOpD,KAAKN,MAAM0D,SAAW,EAC/B,CAMA,kBAAcC,GACZ,OAAOrD,KAAKN,MAAM4D,iBAAmB,EACvC,CAYA,eAAcC,GACZ,OAAOvD,KAAKN,MAAM8D,YACpB,CAmBA,oBAAcC,GAGZ,OAAOzD,MAAKH,GAAkB6D,QAAU1D,KAAKN,MAAM+D,gBACrD,CAMA,aAAcE,GACZ,MAAMC,EAAY5D,KAAKN,MAAMmE,YAAYC,OAAS,CAAA,EAClD,MAAO,IAAKxF,KAAuBsF,EACrC,CAoBA,sBAAcG,GACZ,MAAMC,EAAOhE,KAAKN,MAAMuE,iBAAiBC,WAAWF,MAAQ,iBAG5D,IAAa,IAATA,GAA2B,QAATA,EAAgB,OAAO,EAG7C,IAAa,IAATA,GAA0B,OAATA,EAAe,OAAO,EAG3C,MAAMG,EAAOnE,KAAKuD,YAClB,GAAIY,EAAM,CAER,MAAmB,MADHC,iBAAiBD,GAAME,iBAAiB,2BAA2BC,MAErF,CAEA,OAAO,CACT,CAcA,qBAAcC,GACZ,MAAMJ,EAAOnE,KAAKuD,YAClB,GAAIY,EAAM,CACR,MAAMK,EAAcJ,iBAAiBD,GAAME,iBAAiB,4BAA4BC,OAClFG,EAASC,SAASF,EAAa,IACrC,IAAKG,MAAMF,GAAS,OAAOA,CAC7B,CACA,OAAO,GACT,CAgBU,OAAAG,CAAQC,EAAsBC,EAA0BC,GAEhEF,EAAQG,QAAQC,KAAkBH,EAtmBzBI,QAAQ,WAAY,OAAO7J,cAymBpB,aAAZyJ,EACFD,EAAQG,QAAQG,SAAW,GACN,WAAZL,UACFD,EAAQG,QAAQG,SAIzB,MAAMC,EAASpF,MAAKqF,EAAeP,EAASC,QAE7B,IAAXK,EAEoB,iBAAXA,EACTP,EAAQhI,UAAYN,EAAa6I,GACxBA,aAAkBE,cAC3BT,EAAQhI,UAAY,GACpBgI,EAAQU,YAAYH,EAAOI,WAAU,KAIvCX,EAAQhI,UAAY,EAExB,CAOA,EAAAwI,CAAeP,EAA0BC,GACvC,gBAAIA,EAAqCA,EAClC/E,KAAKN,MAAMmE,YAAYC,QAAQgB,EACxC,CAgBU,mBAAAW,CAAoBC,EAAeC,GAE3CD,EAAKE,cAAc,8CAA8CzH,SAEjE,MAAM0H,EAAYlJ,SAASC,cAAc,QACzCiJ,EAAUC,aAAa,OAAQ,kBAC/BD,EAAUE,UAAY,iBAElBJ,GACFD,EAAKI,aAAa,YAA2B,QAAdH,EAAsB,YAAc,cACnED,EAAKI,aAAa,YAAaH,GAC/B3F,KAAK4E,QAAQiB,EAAyB,QAAdF,EAAsB,UAAY,cAE1DD,EAAKI,aAAa,YAAa,QAC/BJ,EAAKxH,gBAAgB,aACrB8B,KAAK4E,QAAQiB,EAAW,aAI1B,MAAMG,EAAeN,EAAKE,cAAc,oBAAsBF,EAAKE,cAAc,kBAOjF,OANII,EACFN,EAAKM,aAAaH,EAAWG,GAE7BN,EAAKH,YAAYM,GAGZA,CACT,CAgBU,IAAApK,CAAKwK,EAAwC7K,QACrC,IAAZA,EAEFI,QAAQC,KAAKP,EAAiB+K,EAAiC7K,EAAS4E,KAAKuD,YAAY2C,GAAIlG,KAAKnC,OAGlGrC,QAAQC,KAAK,GAAGb,EAAWoF,KAAKuD,YAAY2C,GAAIlG,KAAKnC,SAASoI,IAElE,CAMU,eAAAE,CAAgBhL,EAAsBC,GAC9C,MAAM,IAAI2F,MAAM7F,EAAiBC,EAAMC,EAAS4E,KAAKuD,YAAY2C,GAAIlG,KAAKnC,MAC5E,ECrmCK,MAAMuI,EAEL,gBCVR,SAASC,EAAWnJ,EAAaoJ,GAC/B,GAAKA,EACL,IAAA,MAAW/F,KAAO+F,EAAO,CACvB,MAAMtI,EAAQsI,EAAM/F,GACP,MAATvC,GAAed,EAAG4I,aAAavF,EAAKvC,EAC1C,CACF,CAMA,SAASpB,EACP2J,EACAD,EACAE,GAEA,MAAMtJ,EAAKP,SAASC,cAAc2J,GAclC,OAbAF,EAAWnJ,EAAIoJ,GAaRpJ,CACT,CAKA,SAASuJ,EAAIV,EAAoBO,GAC/B,MAAMpJ,EAAKP,SAASC,cAAc,OAGlC,OAFImJ,MAAcA,UAAYA,GAC9BM,EAAWnJ,EAAIoJ,GACRpJ,CACT,CAOA,SAASwJ,EAAOX,EAAoBO,EAAgCjI,GAClE,MAAMnB,EAAKP,SAASC,cAAc,UAWlC,OAVAM,EAAGyJ,KAAO,SACNZ,MAAcA,UAAYA,GAC9BM,EAAWnJ,EAAIoJ,GAQRpJ,CACT,CAQA,MAAM0J,EAAsBjK,SAASC,cAAc,YA0B5C,SAASiK,IACd,OAAOD,EAAoBvI,QAAQmH,WAAU,EAC/C,CAoBO,SAASsB,EAAaC,GAC3B,MAAMC,EAAWrK,SAASsK,yBAEpBnK,EAAO2J,EAAIM,EAAQG,SAAW,GAAGd,cAA+BA,GAEtE,GAAIW,EAAQG,UAAYH,EAAQI,UAI1BJ,EAAQK,aAAatK,EAAKyI,YAAYwB,EAAQK,aAClDtK,EAAKyI,YAAYwB,EAAQI,eACpB,CAEL,MAAME,EAAiBZ,EAAI,oBAC3BY,EAAe9B,YAAYsB,KAC3B/J,EAAKyI,YAAY8B,EACnB,CAGA,OADAL,EAASzB,YAAYzI,GACdkK,CACT,CAyEO,SAASM,EAAiBP,GAC/B,MAAMQ,EAASd,EAAI,mBAAoB,CAAEe,KAAM,eAAgBC,KAAM,iBAGrE,GAAIV,EAAQW,MAAO,CACjB,MAAMC,EAAUlB,EAAI,mBACpBkB,EAAQC,YAAcb,EAAQW,MAC9BH,EAAOhC,YAAYoC,EACrB,CAGA,MAAMtJ,EAAUoI,EAAI,oBAAqB,CACvCe,KAAM,gBACNC,KAAM,eACN,gCAAiC,KAEnCF,EAAOhC,YAAYlH,GAGnB,MAAMwJ,EAAUpB,EAAI,oBAAqB,CAAEe,KAAM,gBAAiBC,KAAM,iBAGxE,IAAA,MAAWK,KAAOf,EAAQgB,cACpBD,EAAIE,WACNH,EAAQtC,YAAYkB,EAAI,2BAA4B,CAAE,uBAAwBqB,EAAI5B,MAItF,IAAA,MAAW4B,KAAOf,EAAQkB,WACpBH,EAAIE,WACNH,EAAQtC,YAAYkB,EAAI,2BAA4B,CAAE,uBAAwBqB,EAAI5B,MAKtF,MAAMgC,EACJnB,EAAQgB,cAAcrK,KAAMyK,GAAMA,EAAEH,YAAcjB,EAAQkB,WAAWvK,KAAMyK,GAAMA,EAAEH,WAG/EI,EAAarB,EAAQsB,YAAoC,IAAvBtB,EAAQqB,WAMhD,GALIF,GAAoBE,GACtBP,EAAQtC,YAAYkB,EAAI,0BAItB2B,EAAY,CACd,MAAME,EAAY5B,EAAOK,EAAQwB,YAAc,yBAA2B,kBAAmB,CAC3F,oBAAqB,GACrBb,MAAO,WACP,aAAc,wBACd,eAAgBc,OAAOzB,EAAQwB,aAC/B,gBAAiB,mBAEnBD,EAAUtD,QAAQC,KAAO,kBACK,IAA1B8B,EAAQ0B,gBACVH,EAAUzL,UAAYN,EAAawK,EAAQ0B,gBAE7CZ,EAAQtC,YAAY+C,EACtB,CAGA,OADAf,EAAOhC,YAAYsC,GACZN,CACT,CAqCO,SAASmB,EAAe3B,GAC7B,MAAM/C,EAAO+C,EAAQ/C,MAAQ,UACvB2E,EAAOlC,EAAI,iBAAkB,CAAE,YAAazC,IAC5C4E,EAAW7B,EAAQ8B,OAAOzI,OAAS,EACnC0I,EAA0C,IAA1B/B,EAAQ8B,OAAOzI,OAG/B2I,EAActC,EAAI,oBACxBsC,EAAYxD,YAAYsB,KAGxB,IAAImC,EAA8B,KAClC,GAAIJ,EAAU,CACZI,EAAUpM,EAAc,QAAS,CAC/BqM,MAAOlC,EAAQwB,YAAc,sBAAwB,iBACrDf,KAAM,aACN,gBAAiBT,EAAQmC,SACzBzB,KAAM,eACNvB,GAAI,oBAGS,aAATlC,EAAsB,CAAEmF,QAAS,UAAa,CAAA,IAIpD,MAAMC,EAA4C,SAArBrC,EAAQmC,SAAsB,QAAU,OACrEF,EAAQzD,YACNkB,EAAI,wBAAyB,CAC3B,qBAAsB,GACtB,uBAAwB2C,EACxB,cAAe,UAanB,MAAMC,EAAmBC,IACvB,MAAMC,EAAW7C,EAAO4C,EAAS,oDAAsD,uBAAwB,CAC7G9B,KAAM,mBACN,mBAAoB,GACpBE,MAAO,cACP,aAAc,cACd,gBAAiB,mBAGnB,OADA6B,EAAS3B,YAAc,IAChB2B,GAEHC,GAA0C,IAA5BzC,EAAQ0C,iBAA4BX,EACxD,GAAI/B,EAAQ0C,kBAAoBX,EAAe,CAC7C,MAAMY,EAAcjD,EAAI,wBAAyB,CAAEe,KAAM,sBACzDkC,EAAYnE,YAAY8D,GAAgB,IACxCL,EAAQzD,YAAYmE,EACtB,CAGA,MAAMC,EAAelD,EAAI,yBAA0B,CAAEgB,KAAM,iBACrDmC,EAAYnD,EAAI,iBAEtB,IAAA,MAAWoD,KAAS9C,EAAQ8B,OAAQ,CAClC,MACMiB,EAAUrD,EADO,wBAAwBoD,EAAME,WAAa,YAAc,KAAKjB,EAAgB,UAAY,KAAKU,EAAc,oBAAsB,KACtH,CAAE,eAAgBK,EAAM3D,KAGtD8D,EAAYtD,EAAO,uBAAwB,CAC/C,gBAAiB8B,OAAOqB,EAAME,YAC9B,gBAAiB,eAAeF,EAAM3D,OAKxC,GAHI4C,GAAekB,EAAUlE,aAAa,gBAAiB,QAGvD+D,EAAM5E,KAAM,CACd,MAAMgF,EAAWrN,EAAc,OAAQ,CAAEqM,MAAO,uBAChDgB,EAASpN,UAAYN,EAAasN,EAAM5E,MACxC+E,EAAUzE,YAAY0E,EACxB,CAGA,MAAMC,EAAYtN,EAAc,OAAQ,CAAEqM,MAAO,wBAKjD,GAJAiB,EAAUtC,YAAciC,EAAMnC,MAC9BsC,EAAUzE,YAAY2E,IAGjBpB,EAAe,CAClB,MAAMqB,EAAcvN,EAAc,OAAQ,CAAEqM,MAAO,wBAAyB,YAAa,gBAC9D,IAAvBlC,EAAQqD,aACVD,EAAYtN,UAAYN,EAAawK,EAAQqD,aAE/CJ,EAAUzE,YAAY4E,EACxB,CAGA,GAAIX,EAAa,CACf,MAAMa,EAAY5D,EAAI,4BACtB4D,EAAU9E,YAAYyE,GACtBK,EAAU9E,YAAY8D,GAAgB,IACtCS,EAAQvE,YAAY8E,EACtB,MACEP,EAAQvE,YAAYyE,GAItBF,EAAQvE,YACNkB,EAAI,wBAAyB,CAC3BP,GAAI,eAAe2D,EAAM3D,KACzBuB,KAAM,kBAIVmC,EAAUrE,YAAYuE,EACxB,CAEAH,EAAapE,YAAYqE,GACzBZ,EAAQzD,YAAYoE,EACtB,CAWA,MARyB,SAArB5C,EAAQmC,UAAuBF,GACjCL,EAAKpD,YAAYyD,GACjBL,EAAKpD,YAAYwD,KAEjBJ,EAAKpD,YAAYwD,GACbC,GAASL,EAAKpD,YAAYyD,IAGzBL,CACT,CC3bA,SAAS2B,EAAarF,GACpB,OAAKA,EACe,iBAATA,EAA0B1I,EAAa0I,GAE3CA,EAAKsF,UAHM,EAIpB,CA8IO,SAASC,EAAwB7K,GAEtC,QAAIA,GAAQ4H,QAAQG,UAGhB/H,GAAQ4H,QAAQkD,iBAAiBrK,WAGjCT,GAAQ+K,YAAYtK,WAGpBT,GAAQgL,gBAAgBvK,WAGxBT,GAAQ4H,QAAQqD,iBAAiBxK,UAGjCT,GAAQ4H,QAAQsD,2BAGtB,CAYO,SAASC,EAAsBnL,GACpC,OAAmC,IAA5BA,GAAQ4H,QAAQwD,OACzB,CAqfA,SAASC,EAASC,EAAY1H,GAC5B,GAAIA,EAAY2H,SAASD,GAAO,OAAO,EACvC,MAAMnO,EAAOmO,EAAKE,cAClB,OAAOrO,aAAgBsO,YAActO,EAAKqH,OAASZ,CACrD,CAgOO,SAAS8H,EAAoBC,EAAqBC,GAEvD,MAAMC,EAAqBD,EAAME,sBAAsBrL,OAAS,IAAMmL,EAAMG,qBACtEC,EAAmBJ,EAAMZ,eAAeiB,KAAO,EACrD,IAAKJ,IAAuBG,EAAkB,OAE9C,MAAME,EAAcP,EAAW1F,cAAc,sBAC7C,IAAKiG,EAAa,OAGlB,GAAIL,EAAoB,CACtB,IAAA,MAAWtO,KAAMqO,EAAME,sBACrBvO,EAAG4O,MAAMC,QAAU,GACnBF,EAAYtG,YAAYrI,GAE1BqO,EAAMG,sBAAuB,CAC/B,CAGA,MAAMM,EAAiB,IAAIT,EAAMZ,eAAesB,UAAUC,KAAK,CAACC,EAAGhE,KAAOgE,EAAEC,OAAS,MAAQjE,EAAEiE,OAAS,MAExG,IAAA,MAAW/N,KAAW2N,EAAgB,CAEpC,MAAMK,EAAkBd,EAAMe,sBAAsBC,IAAIlO,EAAQ6H,IAC5DmG,IACFA,IACAd,EAAMe,sBAAsBE,OAAOnO,EAAQ6H,KAI7C,IAAIuG,EAAYZ,EAAYjG,cAAc,yBAAyBvH,EAAQ6H,QACtEuG,IACHA,EAAY9P,SAASC,cAAc,OACnC6P,EAAU3G,aAAa,sBAAuBzH,EAAQ6H,IACtD2F,EAAYtG,YAAYkH,IAG1B,MAAMC,EAAUrO,EAAQsO,OAAOF,GAC3BC,GACFnB,EAAMe,sBAAsBM,IAAIvO,EAAQ6H,GAAIwG,EAEhD,CACF,CAMO,SAASG,EACdvB,EACAC,EACAuB,GAEA,GAAKvB,EAAMhD,YAEX,IAAA,MAAYwE,EAASlD,KAAU0B,EAAMb,WAAY,CAC/C,MAAMX,EAAawB,EAAMyB,iBAAiB5P,IAAI2P,GACxCjD,EAAUwB,EAAW1F,cAAc,kBAAkBmH,OACrDlB,EAAc/B,GAASlE,cAAc,0BAE3C,IAAKkE,IAAY+B,EAAa,SAG9B/B,EAAQmD,UAAUC,OAAO,WAAYnD,GACrC,MAAMxC,EAASuC,EAAQlE,cAAc,yBAMrC,GALI2B,GACFA,EAAOzB,aAAa,gBAAiB0C,OAAOuB,IAI1CA,GAEF,GAAoC,IAAhC8B,EAAYrF,SAASpG,OAAc,CAErC,MAAMsM,EAAU7C,EAAM8C,OAAOd,GACzBa,GACFnB,EAAM4B,cAAcP,IAAIG,EAASL,EAErC,MACK,CAEL,MAAMA,EAAUnB,EAAM4B,cAAcZ,IAAIQ,GACpCL,IACFA,IACAnB,EAAM4B,cAAcX,OAAOO,IAE7BlB,EAAYhP,UAAY,EAC1B,CACF,CACF,CAKO,SAASuQ,EAA0B9B,EAAqBC,GAE7D,MAAM8B,EAAc/B,EAAW1F,cAAc,uBACzCyH,IACFA,EAAYJ,UAAUC,OAAO,SAAU3B,EAAMhD,aAC7C8E,EAAYvH,aAAa,eAAgB0C,OAAO+C,EAAMhD,cAE1D,CAKO,SAAS+E,EAAiBhC,EAAqBC,GACpD,MAAM1B,EAAQyB,EAAW1F,cAAc,mBAClCiE,IAELA,EAAMoD,UAAUC,OAAO,OAAQ3B,EAAMhD,aAGhCgD,EAAMhD,cACTsB,EAAMiC,MAAMyB,MAAQ,IAExB,CD17BA3G,EAAoB/J,UAAY,0nBC67BhC,MAAM2Q,EAAuB,0BAO7B,IAAIC,EAAoB,EAGjB,SAASC,IACd,MACiB,oBAARC,KACiB,mBAAjBA,IAAIC,UACXD,IAAIC,SAAS,cAAeJ,EAEhC,CAmCO,SAASK,EACdvC,EACAC,EACAuC,EACAC,GAEA,MAAMlE,EAAQyB,EAAW1F,cAAc,mBACvC,GAAKiE,EAAL,CAOA,GALA0B,EAAMyC,iBAAmBF,EACzBjE,EAAM7E,QAAQiJ,OAASF,EAAS,SAAW,QAIvClE,EAAMqE,aAAa,YAA2C,mBAAtBrE,EAAMsE,YAChD,IACEtE,EAAMsE,aACR,CAAA,MAEA,CAGF,GAAIT,IAA6B,CAK/B,MAAMU,EAAa,GAAGZ,OAA0BC,IAChDlC,EAAM8C,mBAAqBD,EAC3BN,EAAShC,MAAMwC,YAAY,cAAeF,GAC1CvE,EAAMiC,MAAMwC,YAAY,kBAAmBF,GAC3CvE,EAAMiC,MAAMyC,eAAe,OAC3B1E,EAAMiC,MAAMyC,eAAe,QAC3B1E,EAAMiC,MAAMyC,eAAe,QAC7B,MA9DF,SAAkC1E,EAAoBiE,EAAuBC,GAC3E,MAAMS,EAAOV,EAASW,wBAChBvF,EAAWW,EAAM6E,aAAa,kBAAoB,QAKxD,GAJA7E,EAAMiC,MAAM5C,SAAW,QAGvBW,EAAMiC,MAAM6C,IAAM,GAAGC,KAAKC,OAAOd,EAASS,EAAKG,IAAMH,EAAKM,QAAU,OACnD,SAAb5F,EACFW,EAAMiC,MAAMiD,KAAO,GAAGH,KAAKC,MAAML,EAAKO,UACtClF,EAAMiC,MAAMkD,MAAQ,WACf,CACL,MAAMC,EAAkC,oBAAXC,OAAyBA,OAAOC,WAAa,EAC1EtF,EAAMiC,MAAMiD,KAAO,OACnBlF,EAAMiC,MAAMkD,MAAQ,GAAGJ,KAAKC,MAAMI,EAAgBT,EAAKQ,UACzD,CACF,CAgDII,CAAyBvF,EAAOiE,EAAUC,EA5BhC,CA8Bd,CAkFO,SAASsB,EAAmB9D,GAEjC,IAAA,MAAWmB,KAAWnB,EAAM+D,uBAAuBrD,SACjDS,IAEFnB,EAAM+D,uBAAuBC,QAG7B,IAAA,MAAW7C,KAAWnB,EAAM4B,cAAclB,SACxCS,IAEFnB,EAAM4B,cAAcoC,QAGpB,IAAA,MAAW7C,KAAWnB,EAAMe,sBAAsBL,SAChDS,IAEFnB,EAAMe,sBAAsBiD,QAG5BhE,EAAMG,sBAAuB,CAC/B,CAkGA,SAAS8D,EACPC,EACAC,EACA5L,GAEA,MAAM2E,WAAgB3E,GAAOhF,UAA0BwL,EAAaxG,EAAMhF,gBAAa,EACjFsL,WAAatG,GAAOvF,OAAuB+L,EAAaxG,EAAMvF,aAAU,EACxEoR,WAAe7L,GAAOtF,SAAyB8L,EAAaxG,EAAMtF,eAAY,EAG9EwN,EAAiB,IADHyD,EAAYlI,QAAQkD,iBAAmB,IACnByB,KAAK,CAACC,EAAGhE,KAAOgE,EAAEC,OAAS,IAAMjE,EAAEiE,OAAS,IAG9EwD,EAAe,IADHH,EAAY/E,YAAc,IACRwB,KAAK,CAACC,EAAGhE,KAAOgE,EAAEC,OAAS,MAAQjE,EAAEiE,OAAS,MAElF,MAAO,CACLyD,cAAe,CACbnI,MAAO+H,EAAYlI,QAAQG,YAAS,EACpCW,UAAWuH,EAAaxP,OAAS,EACjCmI,YAAamH,EAAanH,YAC1BE,gBACAL,YAAoD,IAAxCqH,EAAYlI,QAAQuI,kBAA+D,IAAlCL,EAAY3Q,WAAWiR,OACpFhI,cAAeiE,EAAepQ,IAAKoU,IAAA,CACjC9J,GAAI8J,EAAE9J,GACN+J,YAAY,EACZjI,YAAagI,EAAErD,UAEjB1E,WAAY,IAEdiI,YAAa,CACXhH,SAAUuG,EAAY3Q,WAAWoK,UAAY,QAC7ClF,KAAMyL,EAAY3Q,WAAWkF,MAAQ,UACrCuE,YAAamH,EAAanH,YAC1B6B,aACAuF,eAMAlG,iBACkC,IAAhCgG,EAAYlI,QAAQwD,UACc,IAAlC0E,EAAY3Q,WAAWiR,QACwB,cAA9CN,EAAY3Q,WAAWkF,MAAQ,WAClC6E,OAAQ+G,EAAahU,IAAKuU,IAAA,CACxBjK,GAAIiK,EAAEjK,GACNwB,MAAOyI,EAAEzI,MACTzC,KAAMqF,EAAa6F,EAAElL,MACrB8E,WAAY2F,EAAa1C,iBAAiB5P,IAAI+S,EAAEjK,QAIxD,CAyEO,SAASkK,EACd9E,EACAmE,EACAC,EACA5L,GAGA,MAAMuM,EAAe/E,EAAW1F,cAAc,IAAIQ,KAC5CkK,EAAkBD,GAAczK,cAAc,qBAGpD,IAAKyK,IAAiBC,EACpB,OAzEG,SACLhF,EACAmE,EACAC,EACA5L,GAEA,MAAMoD,EAAWsD,EAAwBiF,GAQnCc,EAAyB,GAC/B,IAAA,IAASC,EAAiBlF,EAAWmF,WAAYD,EAAGA,EAAIA,EAAEE,YACrC,IAAfF,EAAEG,UAAmBH,EAAcvD,UAAU/B,SAAS9E,IAC1DmK,EAAelT,KAAKmT,GAItBlF,EAAWsF,kBAKX,IAAA,MAAWJ,KAAKD,EACdjF,EAAW/F,YAAYiL,GAGzB,GAAItJ,EAAU,CACZ,MAAM2I,cAAEA,EAAAK,YAAeA,GAAgBV,EAAkBC,EAAcC,EAAc5L,GAI/EkD,EAAWF,EAAa,CAC5BI,UAAU,EACVE,YALkB0D,EAAsB2E,GAAenI,EAAiBuI,QAAiB,EAMzF1I,UALgBuB,EAAewH,KAOjC5E,EAAW/F,YAAYyB,EACzB,KAAO,CACL,MAAMA,EAAWF,EAAa,CAAEI,UAAU,IAC1CoE,EAAW/F,YAAYyB,EACzB,CAEA,OAAOE,CACT,CA0BW2J,CAAwBvF,EAAYmE,EAAaC,EAAc5L,GAIxEwM,EAAgBnS,SAEhB,MAAM+I,EAAWsD,EAAwBiF,GAKzC,GAFAY,EAAaO,kBAET1J,EAAU,CACZmJ,EAAatK,UAAY,GAAGK,cAE5B,MAAMyJ,cAAEA,EAAAK,YAAeA,GAAgBV,EAAkBC,EAAcC,EAAc5L,GAC/EsD,EAAc0D,EAAsB2E,GAAenI,EAAiBuI,QAAiB,EACrF1I,EAAYuB,EAAewH,GAG3BY,EAAe3J,EAAUvB,cAAc,qBACzCkL,GACFA,EAAaC,YAAYT,GAGvBlJ,GAAaiJ,EAAa9K,YAAY6B,GAC1CiJ,EAAa9K,YAAY4B,EAC3B,MAEEkJ,EAAatK,UAAYK,EACzBiK,EAAa9K,YAAY+K,GAG3B,OAAOpJ,CACT,CCj2CO,SAAS8J,EAAsBzF,EAAmB7L,GACvD,IAAIuR,GAAc,EAclB,MAAMC,EAA8B,CAClC,iBAAIC,GACF,OAAOF,CACT,EACA,cAAAG,CAAepT,GACbiT,EAAcjT,CAChB,EAEA,eAAIuK,GACF,OAAOgD,EAAMhD,WACf,EAEA,oBAAIyE,GACF,MAAO,IAAIzB,EAAMyB,iBACnB,EAEA,aAAAqE,CAActE,EAAkBhG,GAC9B,GAA8B,IAA1BwE,EAAMb,WAAWkB,KAEnB,YADArQ,EPvCsB,SOuCS,4BAA6BmE,EAAKwG,IAKnE,IAAIoL,EAUJ,QATgB,IAAZvE,IACExB,EAAMb,WAAWtN,IAAI2P,GACvBuE,EAAkBvE,EAElBxR,EAAeR,EAAsB,eAAegS,eAAsBrN,EAAKwG,KAK/EqF,EAAMhD,aAAe+I,GAAmB/F,EAAMyB,iBAAiB5P,IAAIkU,GACrE,OAKF,GAAI/F,EAAMhD,aAAe+I,IAAoB/F,EAAMyB,iBAAiB5P,IAAIkU,GAEtE,YADAJ,EAAWK,uBAAuBD,GAIpC,GAAI/F,EAAMhD,YAAa,OAQvB,GANAgD,EAAMhD,aAAc,EAMgB,IAAhCgD,EAAMyB,iBAAiBpB,MAAcL,EAAMb,WAAWkB,KAAO,EAC/D,GAAI0F,EACF/F,EAAMyB,iBAAiBwE,IAAIF,OACtB,CACL,MAAMG,EAAgB/R,EAAKuE,iBAAiByN,OAAO5S,WAAW6S,YAC9D,GAAIF,GAAiBlG,EAAMb,WAAWtN,IAAIqU,GACxClG,EAAMyB,iBAAiBwE,IAAIC,OACtB,CACL,MACMG,EADe,IAAIrG,EAAMb,WAAWuB,UAAUC,KAAK,CAACC,EAAGhE,KAAOgE,EAAEC,OAAS,MAAQjE,EAAEiE,OAAS,MAClE,GAC5BwF,GACFrG,EAAMyB,iBAAiBwE,IAAII,EAAW1L,GAE1C,CACF,SACSoL,IAAoB/F,EAAMyB,iBAAiB5P,IAAIkU,GAAkB,CAE1E,IAAA,MAAWpL,IAAM,IAAIqF,EAAMyB,kBACzBzB,EAAMyB,iBAAiBR,OAAOtG,GAEhCqF,EAAMyB,iBAAiBwE,IAAIF,EAC7B,CAGA,MAAMO,EAASnS,EAAKoS,YAQpB,GAPA1E,EAA0ByE,EAAQtG,GAClC+B,EAAiBuE,EAAQtG,GAGzBsB,EAAmBgF,EAAQtG,EAAO7L,EAAKqS,iBAGc,aAAjDrS,EAAKuE,iBAAiByN,OAAO5S,WAAWkF,KAAqB,CAC/D,MAAM9G,GAAEA,EAAA6Q,OAAIA,GA5FlB,SAA+BiE,GAC7B,GAAIA,EAAU,MAAO,CAAE9U,GAAI8U,EAAUjE,QAAQ,GAC7C,MAAMb,EAASxN,EAAKoS,YAAYlM,cAAc,uBAC9C,OAAIsH,EAAe,CAAEhQ,GAAIgQ,EAAQa,QAAQ,GAClC,CAAE7Q,GAAIwC,EAAK8D,aAAcuK,QAAQ,EAC1C,CAuF6BkE,CAAsBlL,GAASkH,QACtDJ,EAAsBgE,EAAQtG,EAAOrO,EAAI6Q,EAC3C,CAGArO,EAAKwS,MAAM,kBAAmB,CAAEC,SAAUjB,EAAWlE,kBACvD,EAEA,cAAAoF,GACE,IAAK7G,EAAMhD,YAAa,OAGxB,GAAI7I,EAAKuE,iBAAiByN,OAAO5S,WAAWiR,OAAQ,OAGpD,IAAA,MAAWrD,KAAWnB,EAAM4B,cAAclB,SACxCS,IAEFnB,EAAM4B,cAAcoC,QAGpB,IAAA,MAAW1F,KAAS0B,EAAMb,WAAWuB,SACnCpC,EAAMwI,YAGR9G,EAAMhD,aAAc,EAGpB,MAAMsJ,EAASnS,EAAKoS,YACpB1E,EAA0ByE,EAAQtG,GAClC+B,EAAiBuE,EAAQtG,GAG4B,aAAjD7L,EAAKuE,iBAAiByN,OAAO5S,WAAWkF,MD+2B3C,SAA+BsH,EAAqBC,GACzD,MAAM1B,EAAQyB,EAAW1F,cAAc,mBACvC,GAAIiE,EAAO,CACT,GAAIA,EAAMqE,aAAa,YAA2C,mBAAtBrE,EAAMyI,YAChD,IACEzI,EAAMyI,aACR,CAAA,MAEA,CAEFzI,EAAMiC,MAAMyC,eAAe,mBAC3B1E,EAAMiC,MAAMyC,eAAe,YAC3B1E,EAAMiC,MAAMyC,eAAe,OAC3B1E,EAAMiC,MAAMyC,eAAe,QAC3B1E,EAAMiC,MAAMyC,eAAe,gBACpB1E,EAAM7E,QAAQiJ,MACvB,CACI1C,EAAMyC,mBACRzC,EAAMyC,iBAAiBlC,MAAMyC,eAAe,eAC5ChD,EAAMyC,iBAAmB,MAE3BzC,EAAM8C,mBAAqB,IAC7B,CCp4BQkE,CAAsBV,EAAQtG,GAIhC7L,EAAKwS,MAAM,mBAAoB,GACjC,EAEA,eAAAM,CAAgBzL,GACVwE,EAAMhD,YACR2I,EAAWkB,iBAEXlB,EAAWG,qBAAyBtK,EAExC,EAEA,oBAAA0L,GACE,IAAKlH,EAAMhD,YAAa,OACxB,GAAqD,aAAjD7I,EAAKuE,iBAAiByN,OAAO5S,WAAWkF,KAAqB,OACjE,MAAM0O,EAASnH,EAAMyC,iBAMrB,GACE0E,GACAA,EAAOC,aACPpH,EAAM8C,oBACNqE,EAAO5G,MAAMzH,iBAAiB,iBAAmBkH,EAAM8C,mBAEvD,OAWF,IAAK9C,EAAM8C,oBAAsBqE,GAAUA,EAAOC,aAAeD,IAAWhT,EAAK8D,aAAc,CAC7F,MAAMqG,EAAQnK,EAAKoS,YAAYlM,cAAc,mBAC7C,GAA8B,UAA1BiE,GAAO7E,QAAQiJ,OAAoB,MACzC,CAOA,MAAM2E,EAAQlT,EAAKoS,YAAYlM,cAAc,uBAC7C,GAAIgN,GAASA,IAAUlT,EAAK8D,aAA5B,CACE,GACE+H,EAAM8C,oBACNX,KDi2BH,SAA8BpC,EAAqBC,EAAmBsH,GAC3E,MAAMzE,EAAa7C,EAAM8C,mBACzB,IAAKD,EAAY,OAAO,EACxB,MAAMvE,EAAQyB,EAAW1F,cAAc,mBACvC,IAAKiE,EAAO,OAAO,EACnB,MAAMiJ,EAAOvH,EAAMyC,iBAcnB,GAbI8E,GAAQA,IAASD,GAASC,EAAKhH,MAAMyC,eAAe,eACxDsE,EAAQ/G,MAAMwC,YAAY,cAAeF,GACzCvE,EAAMiC,MAAMwC,YAAY,kBAAmBF,GAC3CvE,EAAMiC,MAAMyC,eAAe,OAC3B1E,EAAMiC,MAAMyC,eAAe,QAC3B1E,EAAMiC,MAAMyC,eAAe,SAC3B1E,EAAM7E,QAAQiJ,OAAS,QACvB1C,EAAMyC,iBAAmB6E,EAMrBhJ,EAAMqE,aAAa,YAA2C,mBAAtBrE,EAAMsE,YAChD,IACEtE,EAAMsE,aACR,CAAA,MAEA,CAEF,OAAO,CACT,CC33BU4E,CAAqBrT,EAAKoS,YAAavG,EAAOqH,GAE9C,OAEF/E,EAAsBnO,EAAKoS,YAAavG,EAAOqH,GAAO,EAExD,MAEIF,IAAWhT,EAAK8D,cAGlBqK,EAAsBnO,EAAKoS,YAAavG,EAAO7L,EAAK8D,cAAc,EAQtE,EAEA,sBAAA+N,CAAuByB,GACrB,MAAMnJ,EAAQ0B,EAAMb,WAAW6B,IAAIyG,GACnC,IAAKnJ,EAEH,YADAtO,EAAeR,EAAsB,uBAAuBiY,eAAwBtT,EAAKwG,IAK3F,GAA8B,IAA1BqF,EAAMb,WAAWkB,KACnB,OAGF,MAAMiG,EAASnS,EAAKoS,YACd/H,EAAawB,EAAMyB,iBAAiB5P,IAAI4V,GAE9C,GAAIjJ,EAAY,CAEd,MAAM2C,EAAUnB,EAAM4B,cAAcZ,IAAIyG,GACpCtG,IACFA,IACAnB,EAAM4B,cAAcX,OAAOwG,IAE7BnJ,EAAMwI,YACN9G,EAAMyB,iBAAiBR,OAAOwG,GAC9BC,EAA4BpB,EAAQmB,GAAW,EACjD,KAAO,CAEL,IAAA,MAAYE,EAASC,KAAe5H,EAAMb,WACxC,GAAIwI,IAAYF,GAAazH,EAAMyB,iBAAiB5P,IAAI8V,GAAU,CAChE,MAAMxG,EAAUnB,EAAM4B,cAAcZ,IAAI2G,GACpCxG,IACFA,IACAnB,EAAM4B,cAAcX,OAAO0G,IAE7BC,EAAWd,YACX9G,EAAMyB,iBAAiBR,OAAO0G,GAC9BD,EAA4BpB,EAAQqB,GAAS,GAE7C,MAAME,EAAYvB,EAAOjM,cAAc,kBAAkBsN,8BACrDE,MAAqBvW,UAAY,GACvC,CAGF0O,EAAMyB,iBAAiBwE,IAAIwB,GAC3BC,EAA4BpB,EAAQmB,GAAW,GA4IvD,SAAuC1H,EAAqBC,EAAmByH,GAC7E,MAAMnJ,EAAQ0B,EAAMb,WAAW6B,IAAIyG,GACnC,IAAKnJ,GAAO8C,OAAQ,OAEpB,MAAMyG,EAAY9H,EAAW1F,cAAc,kBAAkBoN,8BAC7D,IAAKI,EAAW,OAEhB,MAAM1G,EAAU7C,EAAM8C,OAAOyG,GACzB1G,GACFnB,EAAM4B,cAAcP,IAAIoG,EAAWtG,EAEvC,CAtJQ2G,CAA8BxB,EAAQtG,EAAOyH,EAC/C,CAGAtT,EAAKwS,MAAM,4BAA6B,CAAEhM,GAAI8M,EAAW7N,UAAW4E,GACtE,EAEAuJ,cAAA,IACS,IAAI/H,EAAMb,WAAWuB,UAG9B,iBAAAsH,CAAkB1J,GACZ0B,EAAMb,WAAWtN,IAAIyM,EAAM3D,IAC7B3K,EAAeP,EAAsB,eAAe6O,EAAM3D,yBAA0BxG,EAAKwG,KAG3FqF,EAAMb,WAAWkC,IAAI/C,EAAM3D,GAAI2D,IAO3BoH,GAAevR,EAAK8D,aAAamP,cACnCjT,EAAK8T,uBAET,EAEA,mBAAAC,CAAoB1G,GAElB,GAAIxB,EAAMyB,iBAAiB5P,IAAI2P,GAAU,CACvC,MAAML,EAAUnB,EAAM4B,cAAcZ,IAAIQ,GACpCL,IACFA,IACAnB,EAAM4B,cAAcX,OAAOO,IAE7BxB,EAAMyB,iBAAiBR,OAAOO,EAChC,CAEAxB,EAAMb,WAAW8B,OAAOO,IAEpBkE,GAAevR,EAAK8D,aAAamP,cACnCjT,EAAK8T,sBAET,EAEAE,kBAAA,IACS,IAAInI,EAAMZ,eAAesB,UAGlC,qBAAA0H,CAAsBtV,GAChBkN,EAAMZ,eAAevN,IAAIiB,EAAQ6H,IACnC3K,EAAeN,EAA0B,mBAAmBoD,EAAQ6H,yBAA0BxG,EAAKwG,KAGrGqF,EAAMZ,eAAeiC,IAAIvO,EAAQ6H,GAAI7H,GAEjC4S,EACF5F,EAAoB3L,EAAKoS,YAAavG,GAC7B7L,EAAK8D,aAAamP,aAG3BjT,EAAK8T,uBAET,EAEA,uBAAAI,CAAwBC,GAEtB,MAAMnH,EAAUnB,EAAMe,sBAAsBC,IAAIsH,GAC5CnH,IACFA,IACAnB,EAAMe,sBAAsBE,OAAOqH,IAIrC,MAAMxV,EAAUkN,EAAMZ,eAAe4B,IAAIsH,GACzCxV,GAASyV,cAETvI,EAAMZ,eAAe6B,OAAOqH,GAG5B,MAAM3W,EAAKwC,EAAKoS,YAAYlM,cAAc,yBAAyBiO,OACnE3W,GAAIiB,QACN,EAEA4V,mBAAA,IACS,IAAIxI,EAAMd,gBAAgBwB,UAAUC,KAAK,CAACC,EAAGhE,KAAOgE,EAAEC,OAAS,IAAMjE,EAAEiE,OAAS,IAGzF,sBAAA4H,CAAuB3V,GACjBkN,EAAMd,gBAAgBrN,IAAIiB,EAAQ6H,IACpC3K,EPzViC,SOyVS,oBAAoB8C,EAAQ6H,yBAA0BxG,EAAKwG,KAGvGqF,EAAMd,gBAAgBmC,IAAIvO,EAAQ6H,GAAI7H,IAElC4S,GAAevR,EAAK8D,aAAamP,cACnCjT,EAAK8T,uBAET,EAEA,wBAAAS,CAAyBJ,GAEvB,MAAMnH,EAAUnB,EAAM+D,uBAAuB/C,IAAIsH,GAC7CnH,IACFA,IACAnB,EAAM+D,uBAAuB9C,OAAOqH,IAItC,MAAMxV,EAAUkN,EAAMd,gBAAgB8B,IAAIsH,GACtCxV,GAASyV,WACXzV,EAAQyV,YAGVvI,EAAMd,gBAAgB+B,OAAOqH,GAEzB5C,GACFvR,EAAK8T,sBAET,GAGF,OAAOtC,CACT,CAKA,SAAS+B,EAA4B3H,EAAqB0H,EAAmB7N,GAC3E,MAAM2E,EAAUwB,EAAW1F,cAAc,kBAAkBoN,OACvDlJ,GACFA,EAAQmD,UAAUC,OAAO,WAAY/H,EAEzC,CC9cO,MAAM+O,UAAoBhV,EAEtBrB,KAAO,QAEEyB,w3MAMlBiM,GACA2F,GAKAiD,GACAC,GACAC,GAMAC,GAUAC,GACAC,IAAqB,EAGrB,YAAIC,GACF,YAAuB,IAAhBzU,MAAKuL,CACd,CASA,WAAAmJ,CAAYhV,GACV,GAAIM,MAAKuL,EAAQ,OACjB,MAAMA,EF4BD,CACLb,8BAAgBiK,IAChBhK,kCAAoBgK,IACpBlK,mCAAqBkK,IACrB9J,yBAAyB,EACzBY,sBAAuB,GACvBmJ,cAAe,KACfC,wCAA0B1Y,IAC1B2Y,4CAA8B3Y,IAC9B4Y,6CAA+B5Y,IAC/B6Y,mCAAqB7Y,IACrB8Y,uCAAyB9Y,IACzBoM,aAAa,EACbyE,oCAAsB7Q,IACtBmQ,yCAA2BqI,IAC3BxH,iCAAmBwH,IACnBrF,0CAA4BqF,IAC5BjJ,sBAAsB,EACtBsC,iBAAkB,KAClBK,mBAAoB,ME9CpBrO,MAAKuL,EAASA,EACdvL,MAAKkR,EAAcF,EAAsBzF,EAAO7L,EAClD,CAGA,cAAIwV,GACF,OAAOlV,MAAKuL,CACd,CAGA,mBAAI4J,GACF,OAAOnV,MAAKkR,CACd,CAUA,mBAAIkE,GACF,QAAOpV,KAAKyU,UAAWzU,KAAKmV,gBAAgB5M,WAC9C,CAGA,6BAAI8M,GACF,OAAOrV,KAAKyU,SAAWzU,KAAKmV,gBAAgBnI,iBAAmB,EACjE,CAQA,aAAAqE,CAActE,EAAkBhG,GAC9B/G,KAAKmV,iBAAiB9D,cAActE,EAAShG,EAC/C,CAGA,cAAAqL,GACEpS,KAAKmV,iBAAiB/C,gBACxB,CAOA,eAAAI,CAAgBzL,GACd/G,KAAKmV,iBAAiB3C,gBAAgBzL,EACxC,CAGA,sBAAAwK,CAAuByB,GACrBhT,KAAKmV,iBAAiB5D,uBAAuByB,EAC/C,CAGA,aAAAM,GACE,OAAOtT,KAAKyU,SAAWzU,KAAKmV,gBAAgB7B,gBAAkB,EAChE,CAGA,iBAAAC,CAAkB1J,GACX7J,KAAKyU,WACVzU,KAAKkV,WAAWF,gBAAgBxD,IAAI3H,EAAM3D,IAC1ClG,KAAKmV,gBAAgB5B,kBAAkB1J,GACzC,CAGA,mBAAA4J,CAAoB1G,GACb/M,KAAKyU,WACVzU,KAAKkV,WAAWF,gBAAgBxI,OAAOO,GACvC/M,KAAKmV,gBAAgB1B,oBAAoB1G,GAC3C,CAGA,iBAAA2G,GACE,OAAO1T,KAAKyU,SAAWzU,KAAKmV,gBAAgBzB,oBAAsB,EACpE,CAGA,qBAAAC,CAAsBtV,GACf2B,KAAKyU,WACVzU,KAAKkV,WAAWD,oBAAoBzD,IAAInT,EAAQ6H,IAChDlG,KAAKmV,gBAAgBxB,sBAAsBtV,GAC7C,CAGA,uBAAAuV,CAAwBC,GACjB7T,KAAKyU,WACVzU,KAAKkV,WAAWD,oBAAoBzI,OAAOqH,GAC3C7T,KAAKmV,gBAAgBvB,wBAAwBC,GAC/C,CAGA,kBAAAE,GACE,OAAO/T,KAAKyU,SAAWzU,KAAKmV,gBAAgBpB,qBAAuB,EACrE,CAGA,sBAAAC,CAAuB3V,GACrB2B,KAAKmV,iBAAiBnB,uBAAuB3V,EAC/C,CAGA,wBAAA4V,CAAyBJ,GACvB7T,KAAKmV,iBAAiBlB,yBAAyBJ,EACjD,CAIS,MAAAzS,CAAO1B,GACd4V,MAAMlU,OAAO1B,GAIbM,KAAK0U,YAAYhV,GAKjB,MAAM6V,EAAU,IAAYvV,KAAKwV,eACjCxV,MAAKsU,EAAsBiB,EAC3B7V,EAAK+V,yBAAyB,kBAAmBF,GACjD7V,EAAK+V,yBAAyB,wBAAyBF,GACvD7V,EAAK+V,yBAAyB,sBAAuBF,EACvD,CAGA,EAAAG,CAAehW,GACb,MAAMyE,EAAOzE,EAAK8D,cF6If,SAA4BW,EAAmBoH,GACpD,MAAMoK,EAAWxR,EAAKyB,cAAc,mBACpC,IAAK+P,EAAU,OAGf,IAAKpK,EAAMqJ,cAAe,CACxB,MAAMlN,EAAQiO,EAASjH,aAAa,SAChChH,IACF6D,EAAMqJ,cAAgBlN,EAE1B,CAGA,MAAMiD,EAAiBgL,EAAS1Y,iBAAiB,2BAC7C0N,EAAevK,OAAS,GAA4C,IAAvCmL,EAAME,sBAAsBrL,SAC3DmL,EAAME,sBAAwBlO,MAAMC,KAAKmN,IAI1CgL,EAAyB7J,MAAMC,QAAU,MAC5C,CEhKI6J,CAAmBzR,EAAMnE,KAAKkV,YFiM3B,SACL/Q,EACAoH,GAIA,MAAMsK,EAAuB1R,EAAKyB,cAAc,kCAChD,IAAKiQ,EAAsB,OAG3BtK,EAAMV,yBAA0B,EAGhC,MAAM3E,EAAK,4BACX,GAAIqF,EAAMwJ,0BAA0B3X,IAAI8I,GAAK,OAK7C,MAAM4P,EAAuC,CAC3C5P,KACAkG,MAAO,EACPO,OAEIzL,IAEA,KAAO2U,EAAqBpF,YAC1BvP,EAAOqE,YAAYsQ,EAAqBpF,YAI1C,MAAO,KACL,KAAOvP,EAAOuP,YACZoF,EAAqBtQ,YAAYrE,EAAOuP,YAG9C,GAGJlF,EAAMd,gBAAgBmC,IAAI1G,EAAI4P,GAC9BvK,EAAMwJ,0BAA0BvD,IAAItL,GAGpC2P,EAAqB/J,MAAMC,QAAU,MACvC,CE5OIgK,CAAyB5R,EAAMnE,KAAKkV,YFyQjC,SACL/Q,EACAoH,EACAyK,GAE0B7R,EAAKlH,iBAAiB,gCAE9BgB,QAAS4G,IACzB,MAAMmE,EAAUnE,EACVqB,EAAK8C,EAAQ0F,aAAa,MAC1BhH,EAAQsB,EAAQ0F,aAAa,SAGnC,IAAKxI,IAAOwB,EAKV,YAJAnM,ENnZiC,SMqZ/B,0DAA0D2K,GAAM,eAAewB,GAAS,OAK5F,MAAMzC,EAAO+D,EAAQ0F,aAAa,cAAW,EACvCuH,EAAUjN,EAAQ0F,aAAa,iBAAc,EAC7CtC,EAAQ1H,SAASsE,EAAQ0F,aAAa,UAAY,MAAO,IAG/D,IAAI/B,EAEJ,MAAMuJ,EAAkBF,IAAkBhN,GAC1C,GAAIkN,EACFvJ,EAASuJ,MACJ,CAIL,MAAM7X,EAAU9B,EAAayM,EAAQnM,UAAUyH,QAC/CqI,EAAUF,IACR,MAAM0J,EAAUxZ,SAASC,cAAc,OAGvC,OAFAuZ,EAAQtZ,UAAYwB,EACpBoO,EAAUlH,YAAY4Q,GACf,IAAMA,EAAQhY,SAEzB,CAGA,MAAMiY,EAAgB7K,EAAMb,WAAW6B,IAAIrG,GAO3C,GAAIkQ,IAAkB7K,EAAMsJ,qBAAqBzX,IAAI8I,GAMnD,OALA3K,EACEP,EACA,eAAekL,mHAEjB8C,EAAQ8C,MAAMC,QAAU,QAsB1B,GAAIqK,EAAe,CACjB,GAAIF,EAAiB,CACnB,MAAMG,GAAsB9K,EAAMuJ,yBAAyB1X,IAAI8I,GACzDoQ,EACJF,EAAchK,QAAUA,GAASgK,EAAcnR,OAASA,GAAQmR,EAAcH,UAAYA,EAW5F,GANAG,EAAczJ,OAASA,EACvByJ,EAAchK,MAAQA,EACtBgK,EAAcnR,KAAOA,EACrBmR,EAAcH,QAAUA,EACxB1K,EAAMuJ,yBAAyBtD,IAAItL,GAE/BmQ,GAAsBC,EAAc,CACtC,MAAM5J,EAAUnB,EAAM4B,cAAcZ,IAAIrG,GACpCwG,IACFA,IACAnB,EAAM4B,cAAcX,OAAOtG,GAE/B,CACF,CACA,MACF,CAGA,MAAM2D,EAA6B,CACjC3D,KACAwB,QACAzC,OACAgR,UACA7J,QACAO,UAGFpB,EAAMb,WAAWkC,IAAI1G,EAAI2D,GACzB0B,EAAMsJ,qBAAqBrD,IAAItL,GAC3BgQ,GACF3K,EAAMuJ,yBAAyBtD,IAAItL,GAIrC8C,EAAQ8C,MAAMC,QAAU,QAE5B,CEpYIwK,CAAwBpS,EAAMnE,KAAKkV,WAAYxV,EAAK8W,+BACtD,CAUS,YAAAhB,GACP,MAAM9V,EAAOM,KAAKN,KAClB,IAAKA,IAASM,KAAKyU,SAAU,OAC7B,MAAMlJ,EAAQvL,KAAKkV,WACbuB,EAAWlL,EAAMqJ,cACjB8B,EAAiBnL,EAAMV,wBAC7B7K,MAAK0V,EAAehW,GACpB,MAAMiX,EAAWpL,EAAMqJ,cACjBgC,EAAiBrL,EAAMV,yBAExB8L,IAAaF,GAAcG,IAAmBF,KACjDhX,EAAKmX,sBAKDnX,EAAKoS,YAAYlM,cAAc,uBACjCyJ,EAAmB9D,GACnBvL,KAAK8W,yBAGX,CASS,aAAApB,GACP,MAAMhW,EAAOM,KAAKN,KACbA,GAASM,KAAKyU,UACnBzU,MAAK0V,EAAehW,EACtB,CAGS,sBAAAqX,GACF/W,KAAKyU,UACVpF,EAAmBrP,KAAKkV,WAC1B,CAQS,uBAAA8B,GACP,MAAMtX,EAAOM,KAAKN,KAClB,IAAKA,IAASM,KAAKyU,SAAU,OAAO,EACpC,MAAM3X,EAAO4C,EAAKoS,YACZmF,IAAana,EAAK8I,cAAc,cAChCsR,IAAiBpa,EAAK8I,cAAc,mBACpCuR,EAA0Bra,EAAKG,iBAAiB,0BAA0BmD,OAC1EgX,EAAgBta,EAAK8I,cAAc,oBAA2CZ,QAAQkE,UAAY,QAClGmO,EAAYva,EAAK8I,cAAc,oBAA2CZ,QAAQhB,MAAQ,UAE1F0N,EAAQhS,EAAKuE,iBAAiByN,MAOpC,OACEuF,IAPoBzM,EAAwBkH,KAQ1CwF,IAPsBxF,GAAOhH,YAAYtK,QAAU,GAAK,GAQzD8W,IAPoBxF,GAAOhH,YAAYtK,QAAU,KAOd+W,GACnCD,GAAgBE,KAPC1F,GAAO5S,WAAWoK,UAAY,UAQ/CgO,GAAgBG,KAPH3F,GAAO5S,WAAWkF,MAAQ,UAS5C,CAwBA,EAAAsT,CAAyB5X,GACvB,MAAM6L,EAAQvL,KAAKkV,WACbra,EAAS6E,EAAK8D,aAAa0C,GAGjC,IAAA,MAAW6G,IAAW,IAAIxB,EAAMb,WAAWjK,QACrC8K,EAAMsJ,qBAAqBzX,IAAI2P,IAAYxB,EAAMyJ,gBAAgB5X,IAAI2P,KACzExB,EAAM4B,cAAcZ,IAAIQ,EAAxBxB,KACAA,EAAM4B,cAAcX,OAAOO,GAC3BxB,EAAMb,WAAW8B,OAAOO,IAG1B,IAAA,MAAW8G,IAAa,IAAItI,EAAMZ,eAAelK,QAC3C8K,EAAM0J,oBAAoB7X,IAAIyW,KAClCtI,EAAMe,sBAAsBC,IAAIsH,EAAhCtI,KACAA,EAAMe,sBAAsBE,OAAOqH,GACnCtI,EAAMZ,eAAe6B,OAAOqH,IAG9B,MAAMnJ,WAAEA,EAAAC,eAAYA,GAAmBjL,EAAK6X,4BAE5C,IAAA,MAAW1N,KAASa,EAIda,EAAMyJ,gBAAgB5X,IAAIyM,EAAM3D,MAChCqF,EAAMsJ,qBAAqBzX,IAAIyM,EAAM3D,MACvC3K,EACEP,EACA,eAAe6O,EAAM3D,sFACrBrL,GAEF0Q,EAAM4B,cAAcZ,IAAI1C,EAAM3D,GAA9BqF,KACAA,EAAM4B,cAAcX,OAAO3C,EAAM3D,IACjCqF,EAAMsJ,qBAAqBrI,OAAO3C,EAAM3D,IACxCqF,EAAMuJ,yBAAyBtI,OAAO3C,EAAM3D,KAE9CqF,EAAMb,WAAWkC,IAAI/C,EAAM3D,GAAI2D,IAGjC,IAAA,MAAWxL,KAAWsM,EAChBY,EAAM0J,oBAAoB7X,IAAIiB,EAAQ6H,MACtCqF,EAAMZ,eAAevN,IAAIiB,EAAQ6H,MACnC3K,EACEN,EACA,mBAAmBoD,EAAQ6H,2EAC3BrL,GAEF0Q,EAAMe,sBAAsBC,IAAIlO,EAAQ6H,GAAxCqF,KACAA,EAAMe,sBAAsBE,OAAOnO,EAAQ6H,KAE7CqF,EAAMZ,eAAeiC,IAAIvO,EAAQ6H,GAAI7H,GAEzC,CAES,aAAAmZ,CAAc7X,GACrB,IAAKK,KAAKyU,SAAU,OACpB,MAAMlJ,EAAQvL,KAAKkV,WAKbxV,EAAOM,KAAKN,KACdA,IACFM,MAAK0V,EAAehW,GACpBM,MAAKsX,EAAyB5X,IAShC,MAAM+X,EAAOzX,KAAKC,eAsBlB,GAnBAN,EAAO+R,MAAQ,IAAK+F,KAAS9X,EAAO+R,OACpC/R,EAAO+R,MAAMnK,OAAS,IAAKkQ,EAAKlQ,UAAW5H,EAAO+R,MAAMnK,QAGpDgE,EAAMqJ,gBAAkBjV,EAAO+R,MAAMnK,OAAOG,QAC9C/H,EAAO+R,MAAMnK,OAAOG,MAAQ6D,EAAMqJ,eAIhCrJ,EAAME,uBAAuBrL,OAAS,IACxCT,EAAO+R,MAAMnK,OAAOqD,gBAAkBW,EAAME,uBAI1CF,EAAMV,0BACRlL,EAAO+R,MAAMnK,OAAOsD,yBAA0B,GAI5CU,EAAMb,WAAWkB,KAAO,EAAG,CAC7B,MAAM/C,EAAStL,MAAMC,KAAK+N,EAAMb,WAAWuB,UAC3CpD,EAAOqD,KAAK,CAACC,EAAGhE,KAAOgE,EAAEC,OAAS,MAAQjE,EAAEiE,OAAS,MACrDzM,EAAO+R,MAAMhH,WAAa7B,CAC5B,CAGA,GAAI0C,EAAMZ,eAAeiB,KAAO,EAAG,CACjC,MAAM8L,EAAWna,MAAMC,KAAK+N,EAAMZ,eAAesB,UACjDyL,EAASxL,KAAK,CAACC,EAAGhE,KAAOgE,EAAEC,OAAS,MAAQjE,EAAEiE,OAAS,MACvDzM,EAAO+R,MAAM/G,eAAiB+M,CAChC,CAKA,MAAMC,EAAcpa,MAAMC,KAAK+N,EAAMd,gBAAgBwB,UAC/C2L,EAAyB5X,KAAKN,MAAMmY,eAAenG,OAAOnK,QAAQkD,iBAAmB,GACrFqN,EAAY,IAAI3b,IAAIyb,EAAuBhc,IAAKoU,GAAMA,EAAE9J,KACxD6R,EAAiB,IAAIH,GAC3B,IAAA,MAAWvZ,KAAWsZ,EACfG,EAAU1a,IAAIiB,EAAQ6H,KACzB6R,EAAe1a,KAAKgB,GAGxB0Z,EAAe7L,KAAK,CAACC,EAAGhE,KAAOgE,EAAEC,OAAS,IAAMjE,EAAEiE,OAAS,IAC3DzM,EAAO+R,MAAMnK,OAAOkD,gBAAkBsN,CACxC,CAcS,qBAAAjB,GACP,MAAMpX,EAAOM,KAAKN,KAClB,IAAKA,IAASM,KAAKyU,SAAU,OAC7B,MAAMlJ,EAAQvL,KAAKkV,WACF9E,EACf1Q,EAAKoS,YACLpS,EAAKuE,iBAAiByN,MACtB,CAAEnJ,YAAagD,EAAMhD,YAAayE,iBAAkBzB,EAAMyB,kBAC1DtN,EAAKuE,iBAAiBH,SAGtB9D,MAAKgY,EAAgBtY,GACrBM,KAAKmV,gBAAgB/D,gBAAe,GACpCpR,MAAKiY,EAAqBvY,GAE9B,CA2BS,WAAAwY,GACP,MAAMxY,EAAOM,KAAKN,KAClB,IAAKA,IAASM,KAAKyU,SAAU,OAC7B,MAAMlJ,EAAQvL,KAAKkV,WAKnBlV,MAAKmY,EAAsBzY,GAKvB6L,EAAMhD,aACRsE,EAAmBnN,EAAKoS,YAAavG,GAIlCA,EAAMhD,aAIXvI,KAAKmV,gBAAgB1C,uBACrBzS,MAAKoY,EAAuB1Y,IAJ1BM,MAAKqY,GAKT,CAOA,EAAAF,CAAsBzY,GACpB,MAAM0H,EAAc1H,EAAKoS,YAAYlM,cAAc,qBACnD,IAAKwB,EAAa,OAElB,MAAMM,EAAQhI,EAAKuE,iBAAiByN,OAAOnK,QAAQG,OAAS1H,KAAKkV,WAAWN,cAE5E,IAAIjN,EAAUP,EAAYxB,cAAc,oBACpC8B,GACGC,IACHA,EAAUhL,SAASC,cAAc,MACjC+K,EAAQ5B,UAAY,kBACpB4B,EAAQ7B,aAAa,OAAQ,eAC7BsB,EAAYpB,aAAa2B,EAASP,EAAYqJ,aAEhD9I,EAAQC,YAAcF,GACbC,GACTA,EAAQxJ,QAEZ,CAGA,EAAAia,CAAuB1Y,GAIgC,aAAjDA,EAAKuE,iBAAiByN,OAAO5S,WAAWkF,KAIZ,oBAArBsU,mBACNtY,MAAKuU,IACRvU,MAAKuU,EAAoB,IAAI+D,iBAAiB,KACvCtY,KAAKN,MAASM,KAAKyU,UAAazU,KAAKkV,WAAW3M,YAIrDvI,KAAKmV,gBAAgB1C,uBAHnBzS,MAAKqY,OAMNrY,MAAKwU,IACRxU,MAAKuU,EAAkBgE,QAAQ7Y,EAAKoS,YAAa,CAAE0G,WAAW,EAAMC,SAAS,IAC7EzY,MAAKwU,GAAqB,IAf1BxU,MAAKqY,GAiBT,CAGA,EAAAA,GACMrY,MAAKuU,GAAqBvU,MAAKwU,IACjCxU,MAAKuU,EAAkBmE,aACvB1Y,MAAKwU,GAAqB,EAE9B,CAeA,EAAAyD,CAAqBvY,GACnB,MAAM6L,EAAQvL,KAAKkV,WACb5J,EAAa5L,EAAKoS,YAClBrC,EAAc/P,EAAKuE,iBAAiByN,MAG1CrG,EAAoBC,EAAYC,GFiQ7B,SACLD,EACA3L,EACA4L,GAGA,MAAMoN,EAAiBhZ,GAAQ4H,QAAQkD,iBAAmB,GACpDmO,EAAgB,IAAIrN,EAAMd,gBAAgBwB,UAC1C6L,EAAY,IAAI3b,IAAIwc,EAAe/c,IAAKoU,GAAMA,EAAE9J,KAChD2S,EAAc,IAAIF,GACxB,IAAA,MAAWta,KAAWua,EACfd,EAAU1a,IAAIiB,EAAQ6H,KACzB2S,EAAYxb,KAAKgB,GAKrB,IAAA,MAAWA,KAAWwa,EAAa,CAEjC,GAAItN,EAAM+D,uBAAuBlS,IAAIiB,EAAQ6H,IAAK,SAClD,IAAK7H,EAAQsO,OAAQ,SAErB,MAAMmM,EAAOxN,EAAW1F,cAAc,0BAA0BvH,EAAQ6H,QACxE,IAAK4S,EAAM,SAEX,MAAMpM,EAAUrO,EAAQsO,OAAOmM,GAC3BpM,GACFnB,EAAM+D,uBAAuB1C,IAAIvO,EAAQ6H,GAAIwG,EAEjD,CACF,CE9RIqM,CAA4BzN,EAAYmE,EAAalE,GAGrD,MAAMyN,EAAevJ,GAAa3Q,UAC5B6S,EAAcqH,GAAcrH,YAC9BA,GAAepG,EAAMb,WAAWtN,IAAIuU,IACtCpG,EAAMyB,iBAAiBwE,IAAIG,GAQ7B,MAAMsH,GACqB,IAAzBD,GAAcjJ,QACiB,SAA/BiJ,GAAcE,mBAEkB,IAA/BF,GAAcE,uBAA8BvH,GAA6BpG,EAAMb,WAAWtN,IAAIuU,IAC5FpG,EAAMhD,aAAegD,EAAMb,WAAWkB,KAAO,GAAKqN,GACrDjZ,KAAKqR,gBAKH9F,EAAMhD,cACR+E,EAAiBhC,EAAYC,GAC7BsB,EAAmBvB,EAAYC,GACrB7L,EAAKuE,gBACHvE,EAAKuE,kBAEjBmJ,EAA0B9B,EAAYC,GAMtCvL,KAAKmV,gBAAgB1C,uBAEzB,CASA,EAAAuF,CAAgBtY,GACd,MAAM4L,EAAa5L,EAAKoS,YAClBrC,EAAc/P,EAAKuE,iBAAiByN,MACpCnG,EAAQvL,KAAKkV,YF9ChB,SACL5J,EACA3L,EACA4L,EACA4N,GAOA,MAAMtR,EAAUyD,EAAW1F,cAAc,sBACrCiC,GACFA,EAAQuR,iBAAiB,QAAUC,IAClBA,EAAEnY,OAGUoY,QAAQ,wBAEjCH,EAAUI,kBAOhB,MAAM1P,EAAQyB,EAAW1F,cAAc,mBACnCiE,GAASsP,EAAUK,cACrB3P,EAAMuP,iBAAiB,QAAUC,IAChBA,EAAEnY,OACNoY,QAAQ,uBACjBH,EAAUK,iBAMhB,MAAM5P,EAAY0B,EAAW1F,cAAc,kBACvCgE,GACFA,EAAUwP,iBAAiB,QAAUC,IACnC,MACM9R,EADS8R,EAAEnY,OACKoY,QAAQ,yBAC9B,GAAI/R,EAAQ,CACV,MAAMuC,EAAUvC,EAAO+R,QAAQ,kBACzBtG,EAAYlJ,GAAS4E,aAAa,gBACpCsE,GACFmG,EAAUM,gBAAgBzG,EAE9B,GAGN,CEHI0G,CAAyBpO,EAAYmE,EAAalE,EAAO,CACvDgO,cAAe,IAAMvZ,KAAKwS,kBAC1BiH,gBAAkBzG,GAAsBhT,KAAKuR,uBAAuByB,GACpEwG,aAAc,IAAMxZ,KAAKoS,mBAK3BpS,MAAKmU,MACLnU,MAAKmU,EFiHF,SACL7I,EACA3L,EACAga,GAEA,MAAM9P,EAAQyB,EAAW1F,cAAc,mBACjCgU,EAAStO,EAAW1F,cAAc,wBAClCuB,EAAYmE,EAAW1F,cAAc,mBAC3C,IAAKiE,IAAU+P,IAAWzS,EAExB,MAAO,OAGT,MAAM+B,EAAWvJ,GAAQb,WAAWoK,UAAY,QAGhD,IAAI2Q,EAAS,EACTC,EAAa,EACbC,EAAW,EACXC,GAAa,EAEjB,MAAMC,EAAeZ,IACnB,IAAKW,EAAY,OACjBX,EAAEa,iBAIF,MAAMC,EAAqB,SAAbjR,EAAsBmQ,EAAEe,QAAUP,EAASA,EAASR,EAAEe,QAC9DC,EAAWzL,KAAK0L,IAAIP,EAAUnL,KAAK2L,IAd1B,IAcwCT,EAAaK,IAEpEtQ,EAAMiC,MAAMyB,MAAQ,GAAG8M,OAGnBG,EAAY,KAChB,IAAKR,EAAY,OACjBA,GAAa,EACbJ,EAAO3M,UAAU9O,OAAO,YACxB0L,EAAMiC,MAAM2O,WAAa,GACzB9d,SAASgM,KAAKmD,MAAM4O,OAAS,GAC7B/d,SAASgM,KAAKmD,MAAM6O,WAAa,GAGjC,MAAMC,EAAa/Q,EAAM4E,wBAAwBlB,MACjDoM,EAASiB,GAETje,SAASke,oBAAoB,YAAaZ,GAC1Ctd,SAASke,oBAAoB,UAAWL,IAGpCM,EAAezB,IACnBA,EAAEa,iBACFF,GAAa,EACbH,EAASR,EAAEe,QACXN,EAAajQ,EAAM4E,wBAAwBlB,MAE3CwM,EAAW5S,EAAUsH,wBAAwBlB,MAAQ,GACrDqM,EAAO3M,UAAUuE,IAAI,YACrB3H,EAAMiC,MAAM2O,WAAa,OACzB9d,SAASgM,KAAKmD,MAAM4O,OAAS,aAC7B/d,SAASgM,KAAKmD,MAAM6O,WAAa,OAEjChe,SAASyc,iBAAiB,YAAaa,GACvCtd,SAASyc,iBAAiB,UAAWoB,IAMvC,OAHAZ,EAAOR,iBAAiB,YAAa0B,GAG9B,KACLlB,EAAOiB,oBAAoB,YAAaC,GACxCne,SAASke,oBAAoB,YAAaZ,GAC1Ctd,SAASke,oBAAoB,UAAWL,GAE5C,CE1L0BO,CAAqBzP,EAAYmE,EAAclC,IACnE7N,EAAK8D,aAAasI,MAAMwC,YAAY,yBAA0B,GAAGf,SAInEvN,MAAKoU,MACLpU,MAAKoU,EFeF,SACL7Q,EACA5D,EACA4L,EACA8G,GAEA,MAAMrO,EAAOrE,GAAQb,WAAWkF,KAKhC,GAAa,aAATA,IAAwBrE,GAAQb,WAAWkc,oBAE7C,MAAO,OAKT,GAAa,SAAThX,EAEF,MAAO,OAGT,MAAMuR,EAAW8D,IACf,IAAK9N,EAAMhD,YAAa,OAaxB,MAAMuF,EAAWvC,EAAMyC,iBACvB,IAAA,MAAW/C,KAAQoO,EAAE4B,eAAgB,CACnC,GAAgB,MAAZnN,GAAoB7C,IAAS6C,EAAU,OAC3C,GACE7C,aAAgBiQ,UACfjQ,EAAKgC,WAAW/B,SAAS,mBAAqBD,EAAKkQ,UAAU,yBAC9DnQ,EAASC,EAAM1H,GAEf,MAEJ,CAEA8O,KAIF,OADA1V,SAASyc,iBAAiB,YAAa7D,GAChC,IAAM5Y,SAASke,oBAAoB,YAAatF,EACzD,CErEgC6F,CAAyB1b,EAAK8D,aAAciM,EAAalE,EAAO,IAC1FvL,KAAKoS,kBAIPpS,MAAKqU,MACLrU,MAAKqU,EF4EF,SACL1U,EACA4L,EACA8G,GAGA,GAAgC,SAA5B1S,GAAQb,WAAWkF,KAErB,MAAO,OAGT,MAAMuR,EAAW8D,IACD,WAAVA,EAAE9Y,KAAoB8Y,EAAElX,kBACvBoJ,EAAMhD,aACX8J,KAIF,OADA1V,SAASyc,iBAAiB,UAAW7D,GAC9B,IAAM5Y,SAASke,oBAAoB,UAAWtF,EACvD,CE/FiC8F,CAAmB5L,EAAalE,EAAO,IAAMvL,KAAKoS,iBACjF,CAGA,EAAAkJ,GACEtb,MAAKmU,MACLnU,MAAKmU,OAAiB,EACtBnU,MAAKoU,MACLpU,MAAKoU,OAAuB,EAC5BpU,MAAKqU,MACLrU,MAAKqU,OAAwB,CAC/B,CAGS,MAAA9S,GAKP+T,MAAM/T,QACR,CAMA,iBAAAga,GACEvb,MAAKsb,IACLtb,MAAKqY,IACLrY,MAAKuU,OAAoB,EAGzB,MAAM7U,EAAOM,KAAKN,KACdA,GAAQM,MAAKsU,IACf5U,EAAK8b,2BAA2B,mBAChC9b,EAAK8b,2BAA2B,yBAChC9b,EAAK8b,2BAA2B,uBAChCxb,MAAKsU,OAAsB,GAEzBtU,MAAKuL,KF8eN,SAA2BA,GAEhC,IAAA,MAAWmB,KAAWnB,EAAMe,sBAAsBL,SAChDS,IAEFnB,EAAMe,sBAAsBiD,QAG5B,IAAA,MAAW7C,KAAWnB,EAAM4B,cAAclB,SACxCS,IAEFnB,EAAM4B,cAAcoC,QAGpB,IAAA,MAAW7C,KAAWnB,EAAM+D,uBAAuBrD,SACjDS,IAEFnB,EAAM+D,uBAAuBC,QAG7B,IAAA,MAAWlR,KAAWkN,EAAMd,gBAAgBwB,SAC1C5N,EAAQyV,cAIV,GAAIvI,EAAMhD,YACR,IAAA,MAAWyK,KAAazH,EAAMyB,iBAAkB,CAC9C,MAAMnD,EAAQ0B,EAAMb,WAAW6B,IAAIyG,GACnCnJ,GAAOwI,WACT,CAIF9G,EAAMhD,aAAc,EACpBgD,EAAMyB,iBAAiBuC,QAGvBhE,EAAMb,WAAW6E,QACjBhE,EAAMZ,eAAe4E,QACrBhE,EAAMd,gBAAgB8E,QACtBhE,EAAME,sBAAwB,GAG9BF,EAAMsJ,qBAAqBtF,QAC3BhE,EAAMuJ,yBAAyBvF,QAC/BhE,EAAMwJ,0BAA0BxF,QAGhChE,EAAMG,sBAAuB,CAC/B,CE9hBM+P,CAAkBzb,MAAKuL,GACvBvL,MAAKkR,GAAaE,gBAAe,GAErC"}