@toolbox-web/grid 3.10.1 → 3.10.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"shell.umd.js","names":["button","startPointerDrag","BaseGridPlugin","styles","global","factory","exports","module","require","define","amd","globalThis","self","TbwGridPlugin_shell","TbwGrid","this","_toolbox_web_grid","Object","defineProperty","Symbol","toStringTag","value","TOOL_PANEL_NOT_FOUND","TOOL_PANEL_DUPLICATE","HEADER_CONTENT_DUPLICATE","warnDiagnostic","code","message","gridId","pluginName","console","warn","gridPrefix","toLowerCase","docsUrl","formatDiagnostic","_F","split","map","s","reverse","join","DANGEROUS_TAGS","RegExp","slice","Set","DANGEROUS_ATTR_PATTERN","URL_ATTRS","DANGEROUS_URL_PROTOCOL","setSanitizedHTML","el","html","indexOf","replaceChildren","template","document","createElement","innerHTML","sanitizeNode","content","sanitizeToFragment","textContent","root","toRemove","elements","querySelectorAll","tagName","has","push","namespaceURI","Array","from","attributes","some","attr","test","name","attrsToRemove","attrName","forEach","removeAttribute","remove","supportsPopover","HTMLElement","prototype","showPopover","button$1","label","text","type","className","setAttribute","createSizePopover","options","step","target","input","anchorEl","opened","disposed","abort","AbortController","close","hidePopover","hidden","sync","String","Math","round","getSize","apply","size","Number","isFinite","setSize","addEventListener","e","composedPath","includes","capture","signal","key","element","open","next","anchor","id","minus","plus","reset","field","min","valueAsNumber","relatedTarget","Node","contains","append","build","host","getHost","body","parentNode","appendChild","getLabel","a","getBoundingClientRect","box","clamp","v","max","left","width","window","innerWidth","below","bottom","above","top","height","tall","innerHeight","style","place","focus","dispose","GridClasses","applyAttrs","attrs","tag","children","child","createTextNode","div","button","gridContentTemplate","cloneGridContent","cloneNode","buildGridDOM","fragment","createDocumentFragment","hasShell","shellBody","shellHeader","contentWrapper","buildShellHeader","header","part","role","title","titleEl","toolbar","btn","configButtons","hasRender","apiButtons","hasCustomContent","b","showToggle","hasPanels","toggleBtn","isPanelOpen","dataset","icon","toolPanelIcon","buildPanelCloseButton","inline","closeBtn","buildPanelCloseRow","panelHeader","buildAccordionSection","panel","isSinglePanel","inlineClose","renderHeader","canInlineClose","section","isExpanded","headerBtn","iconSpan","class","titleSpan","chevronSpan","expandIcon","buildAccordionHeader","headerRow","buildShellBody","mode","gridContent","panelEl","panels","length","position","popover","tabindex","showCloseButton","panelContent","accordion","buildToolPanel","iconToString","sanitizeHTML","outerHTML","shouldRenderShellHeader","config","toolbarContents","toolPanels","headerContents","lightDomContent","hasToolButtonsContainer","shouldRenderHeaderBar","visible","isInGrid","node","gridElement","getRootNode","ShadowRoot","setupToolPanelResize","renderRoot","onResize","querySelector","handle","toolPanel","startX","startWidth","maxWidth","isResizing","moved","cancelDrag","sizePopover","availableMax","onMove","preventDefault","delta","clientX","newWidth","finish","commit","classList","transition","cursor","userSelect","finalWidth","onPointerDown","pointerType","add","startPointerDrag","onEnd","wasTap","clamped","closest","onCancel","removeEventListener","renderHeaderContent","state","hasLightDomContent","lightDomHeaderContent","lightDomContentMoved","hasPluginContent","contentArea","display","sortedContents","values","sort","order","existingCleanup","headerContentCleanups","get","delete","container","cleanup","render","set","renderPanelContent","_icons","panelId","expandedSections","toggle","panelCleanups","updateToolbarActiveStates","panelToggle","updatePanelState","DROPDOWN_ANCHOR_NAME","dropdownAnchorSeq","supportsAnchorPositioning","CSS","supports","showToolPanelDropdown","corner","dropdownAnchorEl","hasAttribute","anchorName","dropdownAnchorName","setProperty","removeProperty","rect","getAttribute","right","viewportWidth","positionDropdownFallback","prepareForRerender","toolbarContentCleanups","clear","buildShellOptions","shellConfig","runtimeState","icons","expand","collapseIcon","collapse","sortedPanels","headerOptions","toolPanelToggle","locked","c","hasElement","bodyOptions","p","rebuildShellDOM","existingRoot","existingContent","preservedNodes","n","firstChild","nextSibling","nodeType","buildGridDOMIntoElement","freshContent","replaceWith","createShellController","grid","initialized","controller","isInitialized","setInitialized","openToolPanel","targetSectionId","toggleToolPanelSection","defaultOpenId","effectiveConfig","shell","defaultOpen","firstPanel","shadow","_renderRoot","_accordionIcons","explicit","_hostElement","resolveDropdownAnchor","_emit","sections","closeToolPanel","onClose","hideToolPanelDropdown","toggleToolPanel","reanchorOpenDropdown","stored","isConnected","getPropertyValue","fresh","trigger","prev","repairDropdownAnchor","sectionId","updateAccordionSectionState","otherId","otherPanel","contentEl","renderAccordionSectionContent","expanded","getToolPanels","registerToolPanel","refreshShellHeader","unregisterToolPanel","getHeaderContents","registerHeaderContent","unregisterHeaderContent","contentId","onDestroy","getToolbarContents","registerToolbarContent","unregisterToolbarContent","ShellPlugin","BaseGridPlugin","styles","resizeCleanup","clickOutsideCleanup","escapeDismissCleanup","shellChangeHandler","reanchorObserver","reanchorObserving","hasState","ensureState","Map","lightDomTitle","lightDomToolPanelIds","adapterBoundToolPanelIds","lightDomToolbarContentIds","apiToolPanelIds","apiHeaderContentIds","shellState","shellController","isToolPanelOpen","expandedToolPanelSections","attach","super","handler","syncLightDom","_registerLightDomHandler","parseLightDom","headerEl","parseLightDomShell","rendererFactory","toolButtonsContainer","contentDef","parseLightDomToolButtons","tooltip","parseInt","adapterRenderer","rawContent","trim","wrapper","existingPanel","firstAdapterAttach","attrsChanged","parseLightDomToolPanels","_getToolPanelRendererFactory","hadTitle","hadToolButtons","hasTitle","hasToolButtons","_requestConfigMerge","afterStructuralRender","beforeStructuralRender","needsStructuralRerender","hadShell","hadToolPanel","accordionSectionsBefore","prevPosition","prevMode","syncPluginContributions","keys","_pluginShellContributions","processConfig","ctor","resolvedConfig","contents","apiContents","originalConfigContents","_sourceConfig","configIds","mergedContents","setupListeners","renderShellContents","afterRender","syncShellHeaderTitle","startReanchorObserver","stopReanchorObserver","insertBefore","MutationObserver","observe","childList","subtree","disconnect","configContents","stateContents","allContents","slot","renderCustomToolbarContents","toolPanelCfg","shouldOpenOnLoad","initialState","callbacks","onPanelToggle","onPanelClose","onSectionToggle","setupShellEventListeners","closeOnClickOutside","Element","matches","setupClickOutsideDismiss","defaultPrevented","setupEscapeDismiss","teardownListeners","detach","disposeShellState","_unregisterLightDomHandler","cleanupShellState"],"sources":["../../../../../libs/grid/src/lib/core/internal/diagnostics.ts","../../../../../libs/grid/src/lib/core/internal/sanitize.ts","../../../../../libs/grid/src/lib/core/internal/size-popover.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/shell.css?inline","../../../../../libs/grid/src/lib/plugins/shell/shell-plugin.ts"],"sourcesContent":["/**\n * Centralized diagnostic messages for @toolbox-web/grid.\n *\n * Every user-facing warning, error, or info message in the grid has a unique\n * diagnostic code (e.g. `TBW001`). Each code maps to a section on the online\n * troubleshooting page, giving developers a direct link to resolution steps.\n *\n * ## Usage\n *\n * ```ts\n * import { MISSING_BREAKPOINT, warnDiagnostic, throwDiagnostic } from './diagnostics';\n *\n * // Warn with a code\n * warnDiagnostic(MISSING_BREAKPOINT, 'Set a breakpoint...', gridId);\n *\n * // Throw with a code\n * throwDiagnostic(MISSING_ROW_ID, 'Configure getRowId...', gridId);\n * ```\n *\n * Plugins should prefer `this.warn(MISSING_BREAKPOINT, message)` via BaseGridPlugin\n * instead of importing this module directly.\n *\n * @internal\n */\n\n// #region Grid Prefix\n\n/**\n * Build the `[tbw-grid]` or `[tbw-grid#my-id]` log prefix.\n * Pass `pluginName` for a scoped prefix like `[tbw-grid:SelectionPlugin]`.\n */\nexport function gridPrefix(gridId?: string, pluginName?: string): string {\n const id = gridId ? `#${gridId}` : '';\n const plugin = pluginName ? `:${pluginName}` : '';\n return `[tbw-grid${id}${plugin}]`;\n}\n\n// #endregion\n\n// #region Diagnostic Codes\n\n/**\n * Diagnostic codes used across the grid library.\n *\n * Each code is an individual export so that unused codes are tree-shaken\n * from bundles that don't reference them (esbuild can't tree-shake\n * properties from a single object).\n *\n * Naming: TBW + 3-digit number.\n * Ranges:\n * 001–019 Configuration validation (missing plugins, bad config)\n * 020–029 Plugin lifecycle (dependencies, incompatibilities, alias collapse)\n * 030–039 Feature registry\n * 040–049 Row operations (row ID, row mutations)\n * 050–059 Column operations (width, template)\n * 060–069 Rendering (callbacks, formatters, external views)\n * 070–079 Shell (tool panels, header/toolbar content)\n * 080–089 Editing & editors\n * 090–099 Print\n * 100–109 Clipboard\n * 110–119 Plugin-specific (responsive, undo-redo, grouping-columns)\n * 120–129 Style injection\n * 130–139 Attribute parsing\n * 140–149 DataSource / ServerSide\n */\n\n// --- Config validation (001–019) ---\n/** Column uses a plugin-owned property but the plugin is not loaded. */\nexport const MISSING_PLUGIN = 'TBW001' as const;\n/** Grid config uses a plugin-owned property but the plugin is not loaded. */\nexport const MISSING_PLUGIN_CONFIG = 'TBW002' as const;\n/** Plugin config rule violation (error severity). */\nexport const CONFIG_RULE_ERROR = 'TBW003' as const;\n/** Plugin config rule violation (warning severity). */\nexport const CONFIG_RULE_WARN = 'TBW004' as const;\n\n// --- Plugin lifecycle (020–029) ---\n/** Required plugin dependency is missing. */\nexport const MISSING_DEPENDENCY = 'TBW020' as const;\n/** Optional plugin dependency is missing. */\nexport const OPTIONAL_DEPENDENCY = 'TBW021' as const;\n/** Two loaded plugins are incompatible. */\nexport const INCOMPATIBLE_PLUGINS = 'TBW022' as const;\n/** Two plugin instances resolve to the same canonical name (alias collapse). */\nexport const PLUGIN_ALIAS_COLLAPSE = 'TBW023' as const;\n/** Error thrown inside a plugin event handler. */\nexport const PLUGIN_EVENT_ERROR = 'TBW024' as const;\n/** Conflicting config values found while merging alias-collapsed plugin instances. */\nexport const PLUGIN_ALIAS_CONFIG_CONFLICT = 'TBW025' as const;\n\n// --- Feature registry (030–039) ---\n/** Feature was re-registered (overwritten). */\nexport const FEATURE_REREGISTERED = 'TBW030' as const;\n/** Feature configured but not imported. */\nexport const FEATURE_NOT_IMPORTED = 'TBW031' as const;\n/** Feature depends on another feature that is not enabled. */\nexport const FEATURE_MISSING_DEP = 'TBW032' as const;\n\n// --- Row operations (040–049) ---\n/** Cannot determine row ID (no getRowId and no id property). */\nexport const MISSING_ROW_ID = 'TBW040' as const;\n/** Row with given ID not found. */\nexport const ROW_NOT_FOUND = 'TBW041' as const;\n\n// --- Column operations (050–059) ---\n/** Column has an invalid CSS width value. */\nexport const INVALID_COLUMN_WIDTH = 'TBW050' as const;\n\n// --- Rendering callbacks (060–069) ---\n/** rowClass callback threw an error. */\nexport const ROW_CLASS_ERROR = 'TBW060' as const;\n/** cellClass callback threw an error. */\nexport const CELL_CLASS_ERROR = 'TBW061' as const;\n/** Column format function threw an error. */\nexport const FORMAT_ERROR = 'TBW062' as const;\n/** External view mount() threw an error. */\nexport const VIEW_MOUNT_ERROR = 'TBW063' as const;\n/** External view event dispatch error. */\nexport const VIEW_DISPATCH_ERROR = 'TBW064' as const;\n\n// --- Shell (070–079) ---\n/** Tool panel missing required id. */\nexport const TOOL_PANEL_MISSING_ATTR = 'TBW070' as const;\n/** No tool panels registered. */\nexport const NO_TOOL_PANELS = 'TBW071' as const;\n/** Tool panel section not found. */\nexport const TOOL_PANEL_NOT_FOUND = 'TBW072' as const;\n/** Tool panel already registered. */\nexport const TOOL_PANEL_DUPLICATE = 'TBW073' as const;\n/** Header content already registered. */\nexport const HEADER_CONTENT_DUPLICATE = 'TBW074' as const;\n/** Toolbar content already registered. */\nexport const TOOLBAR_CONTENT_DUPLICATE = 'TBW075' as const;\n\n// --- Editing & editors (080–089) ---\n/** External editor mount() threw an error. */\nexport const EDITOR_MOUNT_ERROR = 'TBW080' as const;\n\n// --- Print (090–099) ---\n/** Print already in progress. */\nexport const PRINT_IN_PROGRESS = 'TBW090' as const;\n/** Grid not available for printing. */\nexport const PRINT_NO_GRID = 'TBW091' as const;\n/** Print operation failed. */\nexport const PRINT_FAILED = 'TBW092' as const;\n/** Multiple elements share the same grid ID (print isolation issue). */\nexport const PRINT_DUPLICATE_ID = 'TBW093' as const;\n\n// --- Clipboard (100–109) ---\n/** Clipboard API write failed. */\nexport const CLIPBOARD_FAILED = 'TBW100' as const;\n\n// --- Plugin-specific (110–119) ---\n/** ResponsivePlugin: no breakpoint configured. */\nexport const MISSING_BREAKPOINT = 'TBW110' as const;\n/** UndoRedoPlugin: transaction already in progress. */\nexport const TRANSACTION_IN_PROGRESS = 'TBW111' as const;\n/** UndoRedoPlugin: no transaction in progress. */\nexport const NO_TRANSACTION = 'TBW112' as const;\n/** GroupingColumnsPlugin: missing id or header on column group definition. */\nexport const COLUMN_GROUP_NO_ID = 'TBW113' as const;\n/** GroupingColumnsPlugin: conflicting columnGroups sources. */\nexport const COLUMN_GROUPS_CONFLICT = 'TBW114' as const;\n\n// --- Style injection (120–129) ---\n/** Failed to extract grid.css from document stylesheets. */\nexport const STYLE_EXTRACT_FAILED = 'TBW120' as const;\n/** Could not find grid.css in document.styleSheets. */\nexport const STYLE_NOT_FOUND = 'TBW121' as const;\n\n// --- Attribute parsing (130–139) ---\n/** Invalid JSON in an HTML attribute. */\nexport const INVALID_ATTRIBUTE_JSON = 'TBW130' as const;\n\n// --- DataSource / ServerSide (140–149) ---\n/** ServerSidePlugin: getRows() rejected. */\nexport const DATASOURCE_FETCH_ERROR = 'TBW140' as const;\n/** ServerSidePlugin: getChildRows() rejected. */\nexport const DATASOURCE_CHILD_FETCH_ERROR = 'TBW141' as const;\n/** ServerSidePlugin: getChildRows() not implemented but a plugin queried for children. */\nexport const DATASOURCE_NO_CHILD_HANDLER = 'TBW142' as const;\n/** ServerSidePlugin: concurrent request limit reached, request deferred. */\nexport const DATASOURCE_THROTTLED = 'TBW143' as const;\n\nexport type DiagnosticCode =\n | typeof MISSING_PLUGIN\n | typeof MISSING_PLUGIN_CONFIG\n | typeof CONFIG_RULE_ERROR\n | typeof CONFIG_RULE_WARN\n | typeof MISSING_DEPENDENCY\n | typeof OPTIONAL_DEPENDENCY\n | typeof INCOMPATIBLE_PLUGINS\n | typeof PLUGIN_ALIAS_COLLAPSE\n | typeof PLUGIN_EVENT_ERROR\n | typeof PLUGIN_ALIAS_CONFIG_CONFLICT\n | typeof FEATURE_REREGISTERED\n | typeof FEATURE_NOT_IMPORTED\n | typeof FEATURE_MISSING_DEP\n | typeof MISSING_ROW_ID\n | typeof ROW_NOT_FOUND\n | typeof INVALID_COLUMN_WIDTH\n | typeof ROW_CLASS_ERROR\n | typeof CELL_CLASS_ERROR\n | typeof FORMAT_ERROR\n | typeof VIEW_MOUNT_ERROR\n | typeof VIEW_DISPATCH_ERROR\n | typeof TOOL_PANEL_MISSING_ATTR\n | typeof NO_TOOL_PANELS\n | typeof TOOL_PANEL_NOT_FOUND\n | typeof TOOL_PANEL_DUPLICATE\n | typeof HEADER_CONTENT_DUPLICATE\n | typeof TOOLBAR_CONTENT_DUPLICATE\n | typeof EDITOR_MOUNT_ERROR\n | typeof PRINT_IN_PROGRESS\n | typeof PRINT_NO_GRID\n | typeof PRINT_FAILED\n | typeof PRINT_DUPLICATE_ID\n | typeof CLIPBOARD_FAILED\n | typeof MISSING_BREAKPOINT\n | typeof TRANSACTION_IN_PROGRESS\n | typeof NO_TRANSACTION\n | typeof COLUMN_GROUP_NO_ID\n | typeof COLUMN_GROUPS_CONFLICT\n | typeof STYLE_EXTRACT_FAILED\n | typeof STYLE_NOT_FOUND\n | typeof INVALID_ATTRIBUTE_JSON\n | typeof DATASOURCE_FETCH_ERROR\n | typeof DATASOURCE_CHILD_FETCH_ERROR\n | typeof DATASOURCE_NO_CHILD_HANDLER\n | typeof DATASOURCE_THROTTLED;\n\n// #endregion\n\n// #region Docs URL\n\nconst DOCS_BASE = 'https://toolboxjs.com/grid/errors';\n\n/** Build a direct link to the troubleshooting section for a code. */\nfunction docsUrl(code: DiagnosticCode): string {\n return `${DOCS_BASE}#${code.toLowerCase()}`;\n}\n\n// #endregion\n\n// #region Formatting\n\n/**\n * Format a diagnostic message with prefix, code, and docs link.\n *\n * Output format:\n * ```\n * [tbw-grid#my-id] TBW001: Your message here.\n *\n * → More info: https://toolboxjs.com/grid/errors#tbw001\n * ```\n */\nexport function formatDiagnostic(code: DiagnosticCode, message: string, gridId?: string, pluginName?: string): string {\n const prefix = gridPrefix(gridId, pluginName);\n return `${prefix} ${code}: ${message}\\n\\n → More info: ${docsUrl(code)}`;\n}\n\n// #endregion\n\n// #region Public API\n\n/**\n * Throw an error with a diagnostic code and docs link.\n * Use for configuration errors and API misuse that should halt execution.\n */\nexport function throwDiagnostic(code: DiagnosticCode, message: string, gridId?: string, pluginName?: string): never {\n throw new Error(formatDiagnostic(code, message, gridId, pluginName));\n}\n\n/**\n * Log a warning with a diagnostic code and docs link.\n * Use for recoverable issues the developer should fix.\n */\nexport function warnDiagnostic(code: DiagnosticCode, message: string, gridId?: string, pluginName?: string): void {\n console.warn(formatDiagnostic(code, message, gridId, pluginName));\n}\n\n/**\n * Log a debug message with a diagnostic code and docs link.\n * Use for optional/soft dependency notifications — visible only when\n * the browser DevTools \"Verbose\" log level is enabled.\n */\nexport function debugDiagnostic(code: DiagnosticCode, message: string, gridId?: string, pluginName?: string): void {\n console.debug(formatDiagnostic(code, message, gridId, pluginName));\n}\n\n/**\n * Log an error with a diagnostic code and docs link.\n * Use for non-throwing errors (e.g., failed async operations).\n */\nexport function errorDiagnostic(code: DiagnosticCode, message: string, gridId?: string, pluginName?: string): void {\n console.error(formatDiagnostic(code, message, gridId, pluginName));\n}\n\n// #endregion\n","// Centralized template expression evaluation & sanitization utilities.\n// Responsible for safely interpolating {{ }} expressions while blocking\n// access to dangerous globals / reflective capabilities.\nimport type { CompiledViewFunction, EvalContext } from '../types';\n\n// #region Constants\nconst EXPR_RE = /{{\\s*([^}]+)\\s*}}/g;\nconst EMPTY_SENTINEL = '__DG_EMPTY__';\nconst SAFE_EXPR = /^[\\w$. '?+\\-*/%:()!<>=,&|]+$/;\n\n// Forbidden identifiers built at runtime to avoid static analysis scanners\n// from flagging string literals (e.g. socket.dev \"network access\" / \"uses eval\").\n// Stored as reversed, pipe-delimited tokens — reversed back when constructing the regex.\nconst _F =\n '__otorp__|__retteGenifed__|__retteSenifed__|rotcurtsnoc|wodniw|sihTlabolg|labolg|ssecorp|noitcnuF|tropmi|lave|tcelfeR|yxorP|rorrE|stnemugra|tnemucod|noitacol|eikooc|egarotSlacol|egarotSnoisses|BDdexedni|hctef|tseuqeRpttHLMX|tekcoSbeW|rekroW|rekroWderahS|rekroWecivreS|renepo|tnerap|pot|semarf|fles'\n .split('|')\n .map((s) => s.split('').reverse().join(''));\nconst FORBIDDEN = new RegExp(`__(proto|defineGetter|defineSetter)|${_F.slice(3).join('|')}|this\\\\b`);\n// #endregion\n\n// #region HTML Sanitization\n\n/**\n * Escape a plain text string for safe insertion into HTML.\n * Converts special HTML characters to their entity equivalents.\n *\n * @param text - Plain text string to escape\n * @returns HTML-safe string\n */\nexport function escapeHtml(text: string): string {\n if (!text || typeof text !== 'string') return '';\n return text\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;')\n .replace(/'/g, '&#39;');\n}\n\n/**\n * Tags that are considered dangerous and will be completely removed.\n * These can execute scripts, load external resources, or manipulate the page.\n */\nconst DANGEROUS_TAGS = new Set(\n 'script|iframe|object|embed|form|input|button|textarea|select|link|meta|base|style|template|slot|portal|frame|frameset|applet|noscript|noembed|plaintext|xmp|listing|math'.split(\n '|',\n ),\n);\n\n/**\n * Attributes that are considered dangerous - event handlers and data loading.\n */\nconst DANGEROUS_ATTR_PATTERN = /^on\\w+$/i;\n\n/**\n * Attributes that can contain URLs which might be javascript: or data: URIs.\n */\nconst URL_ATTRS = new Set('href|src|action|formaction|data|srcdoc|xlink:href|poster|srcset'.split('|'));\n\n/**\n * Protocol patterns that are dangerous in URLs.\n */\nconst DANGEROUS_URL_PROTOCOL = /^\\s*(javascript|vbscript|data|blob):/i;\n\n/**\n * Sanitize an HTML string by removing dangerous tags and attributes.\n *\n * Prefer {@link setSanitizedHTML} wherever the result is destined for an\n * element: this function has to re-serialize the sanitized tree back to a\n * string, and the caller's `innerHTML =` then re-parses it. That\n * parse→serialize→parse round trip is the classic mutation-XSS vector, and it\n * costs a second parse. Use this overload only when a string is genuinely\n * required (e.g. the value is stored, compared, or handed to another API).\n *\n * @param html - Raw HTML string to sanitize\n * @returns Sanitized HTML string safe for innerHTML\n */\nexport function sanitizeHTML(html: string): string {\n if (!html || typeof html !== 'string') return '';\n\n // Fast path: if no HTML tags at all, return as-is (already safe)\n if (html.indexOf('<') === -1) return html;\n\n const template = document.createElement('template');\n template.innerHTML = html;\n\n sanitizeNode(template.content);\n\n return template.innerHTML;\n}\n\n/**\n * Parse and sanitize an HTML string into a detached {@link DocumentFragment}.\n *\n * This is the primitive {@link setSanitizedHTML} builds on, exposed for callers\n * that need to inspect or relocate the nodes before insertion.\n *\n * @param html - Raw HTML string to sanitize\n * @returns Fragment containing the sanitized nodes (empty for falsy input)\n */\nexport function sanitizeToFragment(html: string): DocumentFragment {\n const template = document.createElement('template');\n if (html && typeof html === 'string') {\n template.innerHTML = html;\n sanitizeNode(template.content);\n }\n return template.content;\n}\n\n/**\n * Replace an element's children with the sanitized parse of `html`.\n *\n * **This is the canonical sink for user-supplied markup** — cell/header/group\n * renderer string returns, `gridConfig.icons.*`, light-DOM tool panel fallback\n * content, and any new extension point that turns a user string into markup.\n * `textContent` is wrong here (it renders SVG icon strings as literal text) and\n * raw `innerHTML` is wrong (it is an XSS sink).\n *\n * Nodes are moved from the parse fragment rather than re-serialized, so the\n * sanitized tree is exactly what lands in the document.\n *\n * @param el - Element whose children are replaced\n * @param html - Raw HTML string to sanitize and insert\n */\nexport function setSanitizedHTML(el: Element, html: string): void {\n if (!html || typeof html !== 'string') {\n el.replaceChildren();\n return;\n }\n // Nothing for the parser to do: no markup and no entities to decode.\n if (html.indexOf('<') === -1 && html.indexOf('&') === -1) {\n el.textContent = html;\n return;\n }\n el.replaceChildren(sanitizeToFragment(html));\n}\n\n/**\n * Recursively sanitize a DOM node tree.\n */\nfunction sanitizeNode(root: DocumentFragment | Element): void {\n const toRemove: Element[] = [];\n\n // Use querySelectorAll to find all elements, then filter\n const elements = root.querySelectorAll('*');\n\n for (const el of elements) {\n const tagName = el.tagName.toLowerCase();\n\n // Check if tag is dangerous\n if (DANGEROUS_TAGS.has(tagName)) {\n toRemove.push(el);\n continue;\n }\n\n // SVG elements need special handling - they can contain script-like behavior\n if (tagName === 'svg' || el.namespaceURI === 'http://www.w3.org/2000/svg') {\n // Remove entire SVG if it has any suspicious attributes\n const hasDangerousContent = Array.from(el.attributes).some(\n (attr) => DANGEROUS_ATTR_PATTERN.test(attr.name) || attr.name === 'href' || attr.name === 'xlink:href',\n );\n if (hasDangerousContent) {\n toRemove.push(el);\n continue;\n }\n }\n\n // Check and remove dangerous attributes\n const attrsToRemove: string[] = [];\n for (const attr of el.attributes) {\n const attrName = attr.name.toLowerCase();\n\n // Event handlers (onclick, onerror, onload, etc.)\n if (DANGEROUS_ATTR_PATTERN.test(attrName)) {\n attrsToRemove.push(attr.name);\n continue;\n }\n\n // `is=` upgrades a plain tag into a registered customized built-in, so it\n // can smuggle behaviour past the tag-name check above.\n if (attrName === 'is') {\n attrsToRemove.push(attr.name);\n continue;\n }\n\n // URL attributes with dangerous protocols\n if (URL_ATTRS.has(attrName) && DANGEROUS_URL_PROTOCOL.test(attr.value)) {\n attrsToRemove.push(attr.name);\n continue;\n }\n\n // style attribute can contain expressions (IE) or url() with javascript:\n if (attrName === 'style' && /expression\\s*\\(|javascript:|behavior\\s*:/i.test(attr.value)) {\n attrsToRemove.push(attr.name);\n continue;\n }\n }\n\n attrsToRemove.forEach((name) => el.removeAttribute(name));\n }\n\n // Remove dangerous elements (do this after iteration to avoid modifying during traversal)\n toRemove.forEach((el) => el.remove());\n}\n\n// #endregion\n\n// #region Template Evaluation\n\n// #region Safe Expression Evaluator\n// A minimal recursive-descent evaluator for the token set allowed by SAFE_EXPR.\n// Replaces `new Function()` to avoid dynamic code execution warnings from\n// static analysis tools (socket.dev, npm audit) while maintaining identical\n// functionality for expressions that pass the allowlist guards.\n//\n// Supported grammar (matching SAFE_EXPR = /^[\\w$. '?+\\-*/%:()!<>=,&|]+$/):\n// ternary → logicalOr ('?' ternary ':' ternary)?\n// logicalOr → logicalAnd ('||' logicalAnd)*\n// logicalAnd → equality ('&&' equality)*\n// equality → comparison (('==' | '!=' | '===' | '!==') comparison)*\n// comparison → additive (('<' | '>' | '<=' | '>=') additive)*\n// additive → multiplicative (('+' | '-') multiplicative)*\n// multiplicative → unary (('*' | '/' | '%') unary)*\n// unary → '!' unary | primary\n// primary → NUMBER | STRING | IDENT ('.' IDENT)? | '(' ternary ')'\n\ntype Token =\n { type: 'num'; v: number } | { type: 'str'; v: string } | { type: 'id'; v: string } | { type: 'op'; v: string };\n\nfunction tokenize(expr: string): Token[] | null {\n const tokens: Token[] = [];\n let i = 0;\n while (i < expr.length) {\n const ch = expr[i];\n if (ch === ' ') {\n i++;\n continue;\n }\n\n // Numbers\n if ((ch >= '0' && ch <= '9') || (ch === '.' && i + 1 < expr.length && expr[i + 1] >= '0' && expr[i + 1] <= '9')) {\n const start = i;\n while (i < expr.length && ((expr[i] >= '0' && expr[i] <= '9') || expr[i] === '.')) i++;\n tokens.push({ type: 'num', v: Number(expr.slice(start, i)) });\n continue;\n }\n\n // String literals (single-quoted only, per SAFE_EXPR)\n if (ch === \"'\") {\n let s = '';\n i++; // skip opening quote\n while (i < expr.length && expr[i] !== \"'\") s += expr[i++];\n if (i >= expr.length) return null; // unterminated\n i++; // skip closing quote\n tokens.push({ type: 'str', v: s });\n continue;\n }\n\n // Identifiers (value, row, row.field)\n if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || ch === '_' || ch === '$') {\n const start = i;\n while (i < expr.length && /[\\w$.]/.test(expr[i])) i++;\n tokens.push({ type: 'id', v: expr.slice(start, i) });\n continue;\n }\n\n // Multi-char operators\n if (i + 2 < expr.length) {\n const tri = expr.slice(i, i + 3);\n if (tri === '===' || tri === '!==') {\n tokens.push({ type: 'op', v: tri });\n i += 3;\n continue;\n }\n }\n if (i + 1 < expr.length) {\n const bi = expr.slice(i, i + 2);\n if (bi === '==' || bi === '!=' || bi === '<=' || bi === '>=' || bi === '&&' || bi === '||') {\n tokens.push({ type: 'op', v: bi });\n i += 2;\n continue;\n }\n }\n\n // Single-char operators\n if ('+-*/%<>!?:(),'.includes(ch)) {\n tokens.push({ type: 'op', v: ch });\n i++;\n continue;\n }\n\n return null; // unexpected character\n }\n return tokens;\n}\n\nfunction safeEval(expr: string, ctx: EvalContext): unknown {\n const toks = tokenize(expr);\n if (!toks) return undefined;\n const tokens: Token[] = toks;\n let pos = 0;\n\n function peek(): Token | undefined {\n return tokens[pos];\n }\n function advance(): Token {\n return tokens[pos++];\n }\n function match(type: string, val?: string): boolean {\n const t = peek();\n if (!t) return false;\n if (t.type !== type) return false;\n if (val !== undefined && t.v !== val) return false;\n return true;\n }\n\n function resolveId(name: string): unknown {\n if (name === 'value') return ctx.value;\n if (name.startsWith('row.')) {\n const key = name.slice(4);\n return ctx.row ? ctx.row[key] : undefined;\n }\n if (name.startsWith('typeDefault.')) {\n const key = name.slice('typeDefault.'.length);\n return ctx.typeDefault ? ctx.typeDefault[key] : undefined;\n }\n // bare 'row' reference\n if (name === 'row') return ctx.row;\n if (name === 'typeDefault') return ctx.typeDefault;\n return undefined;\n }\n\n function parseTernary(): unknown {\n const result = parseLogicalOr();\n if (match('op', '?')) {\n advance(); // consume '?'\n const consequent = parseTernary();\n if (!match('op', ':')) return undefined;\n advance(); // consume ':'\n const alternate = parseTernary();\n return result ? consequent : alternate;\n }\n return result;\n }\n\n function parseLogicalOr(): unknown {\n let left = parseLogicalAnd();\n while (match('op', '||')) {\n advance();\n left = left || parseLogicalAnd();\n }\n return left;\n }\n\n function parseLogicalAnd(): unknown {\n let left = parseEquality();\n while (match('op', '&&')) {\n advance();\n left = left && parseEquality();\n }\n return left;\n }\n\n function parseEquality(): unknown {\n let left = parseComparison();\n while (true) {\n const t = peek();\n if (!t || t.type !== 'op') break;\n if (t.v === '===') {\n advance();\n left = left === parseComparison();\n } else if (t.v === '!==') {\n advance();\n left = left !== parseComparison();\n } else if (t.v === '==') {\n advance();\n left = left == parseComparison();\n } else if (t.v === '!=') {\n advance();\n left = left != parseComparison();\n } else break;\n }\n return left;\n }\n\n function parseComparison(): unknown {\n let left = parseAdditive();\n while (true) {\n const t = peek();\n if (!t || t.type !== 'op') break;\n if (t.v === '<') {\n advance();\n left = (left as number) < (parseAdditive() as number);\n } else if (t.v === '>') {\n advance();\n left = (left as number) > (parseAdditive() as number);\n } else if (t.v === '<=') {\n advance();\n left = (left as number) <= (parseAdditive() as number);\n } else if (t.v === '>=') {\n advance();\n left = (left as number) >= (parseAdditive() as number);\n } else break;\n }\n return left;\n }\n\n function parseAdditive(): unknown {\n let left = parseMultiplicative();\n while (true) {\n const t = peek();\n if (!t || t.type !== 'op') break;\n if (t.v === '+') {\n advance();\n left = (left as number) + (parseMultiplicative() as number);\n } else if (t.v === '-') {\n advance();\n left = (left as number) - (parseMultiplicative() as number);\n } else break;\n }\n return left;\n }\n\n function parseMultiplicative(): unknown {\n let left = parseUnary();\n while (true) {\n const t = peek();\n if (!t || t.type !== 'op') break;\n if (t.v === '*') {\n advance();\n left = (left as number) * (parseUnary() as number);\n } else if (t.v === '/') {\n advance();\n left = (left as number) / (parseUnary() as number);\n } else if (t.v === '%') {\n advance();\n left = (left as number) % (parseUnary() as number);\n } else break;\n }\n return left;\n }\n\n function parseUnary(): unknown {\n if (match('op', '!')) {\n advance();\n return !parseUnary();\n }\n return parsePrimary();\n }\n\n function parsePrimary(): unknown {\n const t = peek();\n if (!t) return undefined;\n if (t.type === 'num') {\n advance();\n return t.v;\n }\n if (t.type === 'str') {\n advance();\n return t.v;\n }\n if (t.type === 'id') {\n advance();\n return resolveId(t.v as string);\n }\n if (t.type === 'op' && t.v === '(') {\n advance(); // consume '('\n const result = parseTernary();\n if (match('op', ')')) advance(); // consume ')'\n return result;\n }\n // Unary minus for negative numbers\n if (t.type === 'op' && t.v === '-') {\n advance();\n return -(parsePrimary() as number);\n }\n return undefined;\n }\n\n const result = parseTernary();\n // Ensure all tokens were consumed\n if (pos < tokens.length) return undefined;\n return result;\n}\n// #endregion\n\nexport function evalTemplateString(raw: string, ctx: EvalContext): string {\n if (!raw || raw.indexOf('{{') === -1) return raw; // fast path (no expressions)\n const parts: { expr: string; result: string }[] = [];\n const evaluated = raw.replace(EXPR_RE, (_m, expr) => {\n const res = evalSingle(expr, ctx);\n parts.push({ expr: expr.trim(), result: res });\n // The template markup is author-controlled; the interpolated data is not.\n // Escaping here stops row data from injecting elements or breaking out of\n // an attribute (sanitizeHTML downstream only strips *dangerous* markup).\n return res === EMPTY_SENTINEL ? res : escapeHtml(res);\n });\n const finalStr = postProcess(evaluated);\n // If every part evaluated to EMPTY_SENTINEL we treat this as intentionally blank.\n // If any expression was blocked due to forbidden token (EMPTY_SENTINEL) we *still* only output ''\n // but do not escalate to BLOCKED_SENTINEL unless the original contained explicit forbidden tokens.\n const allEmpty = parts.length && parts.every((p) => p.result === '' || p.result === EMPTY_SENTINEL);\n const hadForbidden = REFLECTIVE_RE.test(raw);\n if (hadForbidden || allEmpty) return '';\n return finalStr;\n}\n\nfunction evalSingle(expr: string, ctx: EvalContext): string {\n expr = (expr || '').trim();\n if (!expr) return EMPTY_SENTINEL;\n if (REFLECTIVE_RE.test(expr)) return EMPTY_SENTINEL;\n if (expr === 'value') return ctx.value == null ? EMPTY_SENTINEL : String(ctx.value);\n if (expr.startsWith('row.') && !/[()?]/.test(expr) && !expr.includes(':')) {\n const key = expr.slice(4);\n const v = ctx.row ? ctx.row[key] : undefined;\n return v == null ? EMPTY_SENTINEL : String(v);\n }\n if (expr.startsWith('typeDefault.') && !/[()?]/.test(expr) && !expr.includes(':')) {\n const key = expr.slice('typeDefault.'.length);\n const v = ctx.typeDefault ? ctx.typeDefault[key] : undefined;\n return v == null ? EMPTY_SENTINEL : String(v);\n }\n if (expr.length > 80) return EMPTY_SENTINEL;\n if (!SAFE_EXPR.test(expr) || FORBIDDEN.test(expr)) return EMPTY_SENTINEL;\n const dotChain = expr.match(/\\./g);\n if (dotChain && dotChain.length > 1) return EMPTY_SENTINEL;\n try {\n const out = safeEval(expr, ctx);\n const str = out == null ? '' : String(out);\n if (REFLECTIVE_RE.test(str)) return EMPTY_SENTINEL;\n return str || EMPTY_SENTINEL;\n } catch {\n return EMPTY_SENTINEL;\n }\n}\n// #endregion\n\n// #region Cell Scrubbing\n/** Pattern matching reflective/introspective APIs that must be stripped from output. */\nconst REFLECTIVE_RE = /Reflect|Proxy|ownKeys/;\n\nfunction postProcess(s: string): string {\n if (!s) return s;\n return s.replace(new RegExp(EMPTY_SENTINEL, 'g'), '').replace(/Reflect\\.[^<>{}\\s]+|\\bProxy\\b|ownKeys\\([^)]*\\)/g, '');\n}\n\nexport function finalCellScrub(cell: HTMLElement): void {\n if (!REFLECTIVE_RE.test(cell.textContent || '')) return;\n // First pass: clear only text nodes containing forbidden tokens\n for (const n of cell.childNodes) {\n if (n.nodeType === Node.TEXT_NODE && REFLECTIVE_RE.test(n.textContent || '')) n.textContent = '';\n }\n // If forbidden tokens persist in element nodes, nuke everything\n if (REFLECTIVE_RE.test(cell.textContent || '')) {\n cell.textContent = '';\n }\n}\n// #endregion\n\n// #region Template Compilation\nexport function compileTemplate(raw: string) {\n if (REFLECTIVE_RE.test(raw)) {\n const fn = (() => '') as CompiledViewFunction;\n fn.__blocked = true;\n return fn;\n }\n const fn = ((ctx: EvalContext) => evalTemplateString(raw, ctx)) as CompiledViewFunction;\n fn.__blocked = false;\n return fn;\n}\n// #endregion\n","/**\n * Non-drag alternative for resizing — WCAG 2.2 SC 2.5.7 \"Dragging Movements\".\n *\n * Dragging a splitter is the primary gesture, but a single-pointer user\n * (trackball, head pointer, eye-gaze, speech mouse emulator) cannot reliably\n * press-hold-move-release. Tapping the splitter instead opens this popover,\n * which offers the same functionality through plain clicks: step buttons, a\n * numeric input, and a reset. A keyboard alternative alone would **not**\n * satisfy the criterion — it is specifically about pointer input.\n *\n * Target-agnostic on purpose: core column resizing and the shell's tool-panel\n * splitter share it, so every resizable thing in the grid presents the same\n * control instead of one-off menus.\n *\n * This module is **internal only** — NOT exported from `src/public.ts`.\n *\n * @since 3.6.0\n * @internal\n */\n\n/** Default size change per click of the − / + buttons, in px. */\nconst DEFAULT_STEP_PX = 16;\n\n/** Gap between the anchor handle and the popover, in px. */\nconst ANCHOR_GAP_PX = 6;\n\n/** Runtime check — happy-dom and older browsers may lack the Popover API. */\nfunction supportsPopover(): boolean {\n return typeof HTMLElement.prototype?.showPopover === 'function';\n}\n\n/**\n * How the popover reads and writes whatever it is sizing. `T` identifies the\n * target — a column index for the header, the panel element for the shell.\n *\n * Supplied by the owning controller so the drag and the click path share one\n * clamping/commit implementation.\n */\nexport interface SizePopoverOptions<T> {\n /** DOM id for the popover element, unique per owning controller. */\n id: string;\n /** Size change per click of the − / + buttons. Defaults to 16px. */\n step?: number;\n /** Current rendered size of the target, in px. */\n getSize(target: T): number;\n /** Commit an explicit size (implementation clamps to its own limits). */\n setSize(target: T, size: number): void;\n /** Restore the target's configured size. */\n reset(target: T): void;\n /** Accessible name for the popover, e.g. `\"Width of column Name\"`. */\n getLabel(target: T): string;\n /**\n * The grid host. The popover is mounted INSIDE it (the grid renders in light\n * DOM) so it inherits the theme's custom properties and `color-scheme`; the\n * top layer keeps it clear of ancestor overflow. `buildBareGridDOMIntoElement`\n * preserves foreign direct children, so it survives re-renders.\n */\n getHost(target: T): HTMLElement | null;\n}\n\nexport interface SizePopover<T> {\n /** Show the popover for `target`, positioned against `anchor`. */\n open(target: T, anchor: HTMLElement): void;\n /** Hide the popover. Safe to call when already closed. */\n close(): void;\n /** Remove the popover element and every document-level listener. */\n dispose(): void;\n /** The popover element, or `null` before the first {@link SizePopover.open}. */\n readonly element: HTMLElement | null;\n}\n\nfunction button(label: string, text: string): HTMLButtonElement {\n const el = document.createElement('button');\n el.type = 'button';\n el.className = 'tbw-size-popover-btn';\n el.setAttribute('aria-label', label);\n el.textContent = text;\n return el;\n}\n\nexport function createSizePopover<T>(options: SizePopoverOptions<T>): SizePopover<T> {\n const step = options.step ?? DEFAULT_STEP_PX;\n let root: HTMLDivElement | null = null;\n let input: HTMLInputElement | null = null;\n let anchorEl: HTMLElement | null = null;\n let target: T;\n let opened = false;\n let disposed = false;\n const abort = new AbortController();\n\n function close(): void {\n if (!opened || !root) return;\n opened = false;\n if (supportsPopover()) root.hidePopover();\n root.hidden = true;\n anchorEl?.removeAttribute('data-tbw-size-open');\n anchorEl = null;\n }\n\n function sync(): void {\n if (input) input.value = String(Math.round(options.getSize(target)));\n }\n\n function apply(size: number): void {\n if (!Number.isFinite(size)) return;\n options.setSize(target, size);\n sync();\n }\n\n function build(): HTMLDivElement {\n const el = document.createElement('div');\n el.id = options.id;\n el.className = 'tbw-size-popover';\n el.setAttribute('popover', 'manual');\n el.setAttribute('role', 'group');\n // Without the Popover API there is no UA `display: none`, so hide it here.\n el.hidden = true;\n\n const minus = button('Narrower', '\\u2212');\n const plus = button('Wider', '+');\n const reset = button('Reset size', 'Reset');\n\n const field = document.createElement('input');\n field.type = 'number';\n field.className = 'tbw-size-popover-input';\n field.min = '0';\n // `step` belongs to the − / + buttons, not the field: a measured size is\n // rarely a multiple of it, and a step mismatch would mark the field\n // `:invalid` and expose `aria-invalid=\"true\"` for a perfectly valid width.\n field.step = 'any';\n field.setAttribute('aria-label', 'Size in pixels');\n\n minus.addEventListener('click', () => apply(options.getSize(target) - step), { signal: abort.signal });\n plus.addEventListener('click', () => apply(options.getSize(target) + step), { signal: abort.signal });\n reset.addEventListener(\n 'click',\n () => {\n options.reset(target);\n sync();\n },\n { signal: abort.signal },\n );\n field.addEventListener('change', () => apply(field.valueAsNumber), { signal: abort.signal });\n\n // Tabbing past either end dismisses instead of stranding the user — the\n // popover has no visible trigger to tab back to. A null `relatedTarget`\n // (click focus in Firefox/Safari, focus leaving the document) is ignored.\n el.addEventListener(\n 'focusout',\n (e) => {\n const next = (e as FocusEvent).relatedTarget;\n if (next instanceof Node && !el.contains(next)) close();\n },\n { signal: abort.signal },\n );\n\n el.append(minus, field, plus, reset);\n input = field;\n return el;\n }\n\n /**\n * Position against the anchor in viewport coordinates. The popover lives in\n * the top layer, so its containing block is the viewport regardless of where\n * the anchor sits in the DOM.\n *\n * Called once per open and never again: repeatedly clicking − / + moves the\n * anchor, and following it would slide the button out from under the pointer\n * of the very users this control exists for. The anchor keeps its indicator\n * lit instead (`data-tbw-size-open`) to show which edge is being sized.\n */\n function place(anchor: HTMLElement): void {\n if (!root) return;\n const a = anchor.getBoundingClientRect();\n const box = root.getBoundingClientRect();\n const clamp = (v: number, max: number) => Math.max(4, Math.min(v, max - 4));\n const left = clamp(a.left + a.width / 2 - box.width / 2, window.innerWidth - box.width);\n\n // A full-height anchor (the tool-panel splitter) has no meaningful \"below\":\n // it would land past the bottom of the grid. Sit against its middle instead.\n const below = a.bottom + ANCHOR_GAP_PX;\n const above = a.top - ANCHOR_GAP_PX - box.height;\n const tall = a.height > box.height * 3;\n let top: number;\n if (!tall && below + box.height <= window.innerHeight) top = below;\n else if (!tall && above >= 4) top = above;\n else top = clamp(a.top + a.height / 2 - box.height / 2, window.innerHeight - box.height);\n\n root.style.left = `${Math.round(left)}px`;\n root.style.top = `${Math.round(top)}px`;\n }\n\n // Any pointer press outside the popover dismisses it. Capture phase so the\n // press that lands on a grid cell still reaches its own handler afterwards.\n document.addEventListener(\n 'pointerdown',\n (e) => {\n if (opened && root && !e.composedPath().includes(root)) close();\n },\n { capture: true, signal: abort.signal },\n );\n document.addEventListener(\n 'keydown',\n (e) => {\n if ((e as KeyboardEvent).key === 'Escape') close();\n },\n { capture: true, signal: abort.signal },\n );\n\n return {\n get element() {\n return root;\n },\n open(next, anchor) {\n if (disposed) return;\n target = next;\n anchorEl?.removeAttribute('data-tbw-size-open');\n anchorEl = anchor;\n anchor.setAttribute('data-tbw-size-open', '');\n if (!root) root = build();\n const host = options.getHost(next) ?? document.body;\n if (root.parentNode !== host) host.appendChild(root);\n root.setAttribute('aria-label', options.getLabel(next));\n sync();\n root.hidden = false;\n if (!opened && supportsPopover()) root.showPopover();\n opened = true;\n place(anchor);\n input?.focus();\n },\n close,\n dispose() {\n disposed = true;\n close();\n abort.abort();\n root?.remove();\n root = null;\n input = null;\n },\n };\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/**\n * CSS selector for focusable editor elements within a cell.\n * Used by EditingPlugin, UndoRedoPlugin, and keyboard navigation. Defined here\n * (a leaf module) rather than in `rows.ts` so `keyboard.ts` can consume it\n * without creating a rows ↔ keyboard import cycle.\n */\nexport const FOCUSABLE_EDITOR_SELECTOR =\n 'input,select,textarea,[contenteditable=\"true\"],[contenteditable=\"\"],[tabindex]:not([tabindex=\"-1\"])';\n\n// #region CSS Classes\n\n/**\n * CSS class names used in the grid's DOM.\n * Use these when adding/removing classes or querying elements.\n *\n * Classes are organized by:\n * - Core: Used by the grid core for structure and basic functionality\n * - Shared: Used by multiple features/plugins, styled by core CSS\n *\n * @category Plugin Development\n * @since 0.1.1\n */\nexport const GridClasses = {\n // ─── Core Structure ───────────────────────────────────────────────\n ROOT: 'tbw-grid-root',\n HEADER: 'header',\n HEADER_ROW: 'header-row',\n HEADER_CELL: 'header-cell',\n\n // Body structure\n ROWS_VIEWPORT: 'rows-viewport',\n ROWS_SPACER: 'rows-spacer',\n ROWS_CONTAINER: 'rows',\n\n // Row elements\n DATA_ROW: 'data-grid-row',\n GROUP_ROW: 'group-row',\n\n // Cell elements\n DATA_CELL: 'cell',\n\n // ─── Core States ──────────────────────────────────────────────────\n SELECTED: 'selected',\n FOCUSED: 'focused',\n EDITING: 'editing',\n EXPANDED: 'expanded',\n COLLAPSED: 'collapsed',\n DRAGGING: 'dragging',\n RESIZING: 'resizing',\n\n // Sorting (core feature)\n SORTABLE: 'sortable',\n SORTED_ASC: 'sorted-asc',\n SORTED_DESC: 'sorted-desc',\n\n // Visibility\n HIDDEN: 'hidden',\n\n // ─── Shared Classes (used by plugins, styled by core CSS) ────────\n // These are defined here because core CSS provides the base styling.\n // Plugins apply these classes; core CSS defines how they look.\n\n // Sticky positioning (PinnedColumnsPlugin applies, core CSS styles)\n STICKY_LEFT: 'sticky-left',\n STICKY_RIGHT: 'sticky-right',\n\n // Pinned rows (PinnedRowsPlugin applies, core CSS styles)\n PINNED_TOP: 'pinned-top',\n PINNED_BOTTOM: 'pinned-bottom',\n\n // Tree structure (TreePlugin applies, core CSS styles)\n TREE_TOGGLE: 'tree-toggle',\n TREE_INDENT: 'tree-indent',\n\n // Grouping (GroupingRowsPlugin applies, core CSS styles)\n GROUP_TOGGLE: 'group-toggle',\n GROUP_LABEL: 'group-label',\n GROUP_COUNT: 'group-count',\n\n // Selection (SelectionPlugin applies, core CSS styles)\n RANGE_SELECTION: 'range-selection',\n SELECTION_OVERLAY: 'selection-overlay',\n} as const;\n\n// #endregion\n\n// #region Data Attributes\n\n/**\n * Data attribute names used on grid elements.\n * Use these when getting/setting data attributes.\n *\n * @category Plugin Development\n * @since 0.1.1\n */\nexport const GridDataAttrs = {\n // ─── Core Attributes ──────────────────────────────────────────────\n FIELD: 'data-field',\n\n /**\n * Marks a `title` the grid itself put on a cell to reveal ellipsised text\n * (WCAG SC 1.4.12). Anything that sets its own `title` on a cell must clear\n * this, or the grid will later mistake that title for its own and replace it.\n *\n * @since 3.6.0\n */\n TRUNCATED: 'data-tbw-truncated',\n\n // ─── Shared Attributes (used by plugins) ──────────────────────────\n GROUP_KEY: 'data-group-key', // GroupingRowsPlugin\n} as const;\n\n// #endregion\n\n// #region Selectors\n\n/**\n * Common CSS selectors for querying grid elements.\n * Built from the class constants for consistency.\n *\n * @category Plugin Development\n * @since 0.1.1\n */\nexport const GridSelectors = {\n ROOT: `.${GridClasses.ROOT}`,\n HEADER: `.${GridClasses.HEADER}`,\n HEADER_ROW: `.${GridClasses.HEADER_ROW}`,\n HEADER_CELL: `.${GridClasses.HEADER_CELL}`,\n ROWS_VIEWPORT: `.${GridClasses.ROWS_VIEWPORT}`,\n ROWS_CONTAINER: `.${GridClasses.ROWS_CONTAINER}`,\n DATA_ROW: `.${GridClasses.DATA_ROW}`,\n DATA_CELL: `.${GridClasses.DATA_CELL}`,\n GROUP_ROW: `.${GridClasses.GROUP_ROW}`,\n\n // By data attribute\n CELL_BY_FIELD: (field: string) => `.${GridClasses.DATA_CELL}[${GridDataAttrs.FIELD}=\"${field}\"]`,\n\n // State selectors\n SELECTED_ROWS: `.${GridClasses.DATA_ROW}.${GridClasses.SELECTED}`,\n EDITING_CELL: `.${GridClasses.DATA_CELL}.${GridClasses.EDITING}`,\n} as const;\n\n// #endregion\n\n// #region CSS Custom Properties\n\n/**\n * CSS custom property names for theming.\n * Use these when programmatically setting styles.\n *\n * @category Plugin Development\n * @since 0.1.1\n */\nexport const GridCSSVars = {\n // Colors\n COLOR_BG: '--tbw-color-bg',\n COLOR_FG: '--tbw-color-fg',\n COLOR_FG_MUTED: '--tbw-color-fg-muted',\n COLOR_BORDER: '--tbw-color-border',\n COLOR_ACCENT: '--tbw-color-accent',\n COLOR_HEADER_BG: '--tbw-color-header-bg',\n COLOR_HEADER_FG: '--tbw-color-header-fg',\n COLOR_SELECTION: '--tbw-color-selection',\n COLOR_ROW_HOVER: '--tbw-color-row-hover',\n COLOR_ROW_ALT: '--tbw-color-row-alt',\n\n // Sizing\n ROW_HEIGHT: '--tbw-row-height',\n HEADER_HEIGHT: '--tbw-header-height',\n CELL_PADDING: '--tbw-cell-padding',\n\n // Typography\n FONT_FAMILY: '--tbw-font-family',\n FONT_SIZE: '--tbw-font-size',\n\n // Borders\n BORDER_RADIUS: '--tbw-border-radius',\n FOCUS_OUTLINE: '--tbw-focus-outline',\n} as const;\n\n// #endregion\n\n// Type helpers\n\n/**\n * Union of all CSS class names used in the grid's DOM.\n * Use `GridClasses` to access individual values by key.\n *\n * @category Plugin Development\n * @since 0.1.1\n */\nexport type GridClassName = (typeof GridClasses)[keyof typeof GridClasses];\n\n/**\n * Union of all `data-*` attribute names used on grid elements.\n * Use `GridDataAttrs` to access individual values by key.\n *\n * @category Plugin Development\n * @since 0.1.1\n */\nexport type GridDataAttr = (typeof GridDataAttrs)[keyof typeof GridDataAttrs];\n\n/**\n * Union of all CSS custom property names (`--tbw-*`) available for theming.\n * Use `GridCSSVars` to access individual values by key.\n *\n * @category Plugin Development\n * @since 0.1.1\n */\nexport type GridCSSVar = (typeof GridCSSVars)[keyof typeof GridCSSVars];\n","/**\n * 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 { setSanitizedHTML } 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 setSanitizedHTML(toggleBtn, 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 * Panel close (✕) button. `data-panel-close` is wired by\n * `setupShellEventListeners` to close the panel.\n */\nfunction buildPanelCloseButton(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\n/** Slim standalone header row holding just the close button. */\nfunction buildPanelCloseRow(): HTMLDivElement {\n const panelHeader = div('tbw-tool-panel-header', { part: 'tool-panel-header' });\n panelHeader.appendChild(buildPanelCloseButton(false));\n return panelHeader;\n}\n\n/** Accordion header button: icon + title + chevron. */\nfunction buildAccordionHeader(\n panel: ShellBodyOptions['panels'][number],\n options: ShellBodyOptions,\n isSinglePanel: boolean,\n): HTMLButtonElement {\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 if (panel.icon) {\n const iconSpan = createElement('span', { class: 'tbw-accordion-icon' });\n setSanitizedHTML(iconSpan, panel.icon);\n headerBtn.appendChild(iconSpan);\n }\n\n // Title (empty span kept as a flex spacer so the chevron stays right-aligned)\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 setSanitizedHTML(chevronSpan, options.expandIcon);\n }\n headerBtn.appendChild(chevronSpan);\n }\n return headerBtn;\n}\n\n/** One accordion section (header row + empty content slot). */\nfunction buildAccordionSection(\n panel: ShellBodyOptions['panels'][number],\n options: ShellBodyOptions,\n isSinglePanel: boolean,\n inlineClose: boolean,\n): HTMLDivElement {\n // The accordion header row (icon + title + chevron/toggle) is skipped only\n // when a single title-less panel is registered — there is nothing to show and\n // no sibling to toggle between. With multiple panels a title-less header still\n // renders (empty title span) so the chevron and toggle button stay available.\n const renderHeader = !!panel.title || !isSinglePanel;\n // Inline close shares the header row; only possible when a header exists.\n const canInlineClose = inlineClose && renderHeader;\n const sectionClasses = `tbw-accordion-section${panel.isExpanded ? ' expanded' : ''}${isSinglePanel ? ' single' : ''}${canInlineClose ? ' has-inline-close' : ''}`;\n const section = div(sectionClasses, { 'data-section': panel.id });\n\n if (renderHeader) {\n const headerBtn = buildAccordionHeader(panel, options, isSinglePanel);\n if (canInlineClose) {\n // Single-panel close button shares the header row with the panel title.\n const headerRow = div('tbw-accordion-header-row');\n headerRow.appendChild(headerBtn);\n headerRow.appendChild(buildPanelCloseButton(true));\n section.appendChild(headerRow);\n } else {\n section.appendChild(headerBtn);\n }\n } else if (inlineClose) {\n // Single title-less panel with the header suppressed: the inline close\n // button has no header row to share, so render it as a standalone row.\n section.appendChild(buildPanelCloseRow());\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 return section;\n}\n\n/** The `<aside>` tool panel: resize handle, optional close affordance, accordion. */\nfunction buildToolPanel(options: ShellBodyOptions, mode: 'overlay' | 'push' | 'dropdown'): HTMLElement {\n const isSinglePanel = options.panels.length === 1;\n const 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 — draggable, and tappable for the SC 2.5.7 size menu.\n panelEl.appendChild(\n div('tbw-tool-panel-resize', {\n 'data-resize-handle': '',\n 'data-handle-position': options.position === 'left' ? 'right' : 'left',\n role: 'button',\n tabindex: '-1',\n 'aria-label': 'Tool panel width — drag, or activate to set a width',\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).\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 inlineClose = options.showCloseButton === true && isSinglePanel;\n if (options.showCloseButton && !isSinglePanel) {\n panelEl.appendChild(buildPanelCloseRow());\n }\n\n const panelContent = div('tbw-tool-panel-content', { role: 'presentation' });\n const accordion = div('tbw-accordion');\n for (const panel of options.panels) {\n accordion.appendChild(buildAccordionSection(panel, options, isSinglePanel, inlineClose));\n }\n panelContent.appendChild(accordion);\n panelEl.appendChild(panelContent);\n return panelEl;\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\n // Grid content wrapper with cloned grid structure\n const gridContent = div('tbw-grid-content');\n gridContent.appendChild(cloneGridContent());\n\n const panelEl = options.panels.length > 0 ? buildToolPanel(options, mode) : null;\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 { startPointerDrag } from '../../core/internal/pointer-drag';\nimport { escapeHtml, sanitizeHTML, setSanitizedHTML } from '../../core/internal/sanitize';\nimport { createSizePopover, type SizePopover } from '../../core/internal/size-popover';\nimport type { IconValue } from '../../core/types';\nimport type { HeaderContentDefinition, ShellConfig, ToolbarContentDefinition, ToolPanelDefinition } from './types';\n\n/** Tool-panel width change per click of the size popover's − / + buttons, in px. */\nconst RESIZE_STEP_PX = 32;\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 * 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 // Skip the header row only for a single title-less panel. With multiple\n // panels a title-less header still renders (empty title span) so the\n // expand/collapse chevron and toggle button remain available.\n const renderHeader = !!panel.title || !isSinglePanel;\n const headerHtml = renderHeader\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\">${escapeHtml(panel.title ?? '')}</span>\n ${chevronHtml}\n </button>`\n : '';\n accordionHtml += `\n <div class=\"${sectionClasses}\" data-section=\"${panel.id}\">\n ${headerHtml}\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}\" role=\"button\" tabindex=\"-1\" aria-label=\"Tool panel width — drag, or activate to set a width\"></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` (optional): Panel title shown in accordion header. Omit it when the\n * panel content provides its own heading (see {@link ToolPanelDefinition.title}).\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') ?? undefined;\n\n // Skip only if the required `id` is missing. `title` is optional (#430):\n // a title-less panel renders its content without an accordion header row\n // (single panel) or with an empty title span (multiple panels).\n if (!id) {\n warnDiagnostic(TOOL_PANEL_MISSING_ATTR, `Tool panel missing required id attribute: title=\"${title ?? ''}\"`);\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 light-DOM markup 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 rawContent = panelEl.innerHTML.trim();\n render = (container: HTMLElement) => {\n const wrapper = document.createElement('div');\n setSanitizedHTML(wrapper, rawContent);\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.title !== title ||\n existingPanel.order !== order ||\n existingPanel.icon !== icon ||\n 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.title = title;\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 *\n * The handle is draggable **and** tappable: pressing and releasing without\n * moving opens a click-only size menu (WCAG 2.2 SC 2.5.7).\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 let moved = false;\n let cancelDrag: (() => void) | null = null;\n let sizePopover: SizePopover<HTMLElement> | null = null;\n\n const availableMax = () => shellBody.getBoundingClientRect().width - 20;\n\n const openSizePopover = () => {\n sizePopover ??= createSizePopover<HTMLElement>({\n id: 'tbw-tool-panel-width-popover',\n step: RESIZE_STEP_PX,\n getSize: (el) => el.getBoundingClientRect().width,\n // Same clamp as `onMove`, so no pointer path can reach a size the drag cannot.\n setSize: (el, width) => {\n const clamped = Math.min(availableMax(), Math.max(minWidth, width));\n el.style.width = `${clamped}px`;\n onResize(clamped);\n },\n reset: (el) => {\n el.style.width = '';\n onResize(el.getBoundingClientRect().width);\n },\n getLabel: () => 'Tool panel width',\n getHost: (el) => el.closest<HTMLElement>('tbw-grid, [data-tbw-grid]'),\n });\n sizePopover.open(panel, handle);\n };\n\n const onMove = (e: PointerEvent) => {\n if (!isResizing) return;\n e.preventDefault();\n moved = true;\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 /**\n * Restore chrome after a drag. `commit` is false when the drag was aborted\n * (pointercancel / Escape), in which case the caller has already restored the\n * original width and no `onResize` notification should fire.\n */\n const finish = (commit: boolean) => {\n if (!isResizing) return;\n isResizing = false;\n cancelDrag = null;\n handle.classList.remove('resizing');\n panel.style.transition = ''; // Re-enable transition\n document.body.style.cursor = '';\n document.body.style.userSelect = '';\n\n if (commit) {\n // Get final width and notify\n const finalWidth = panel.getBoundingClientRect().width;\n onResize(finalWidth);\n }\n };\n\n const onPointerDown = (e: PointerEvent) => {\n if (isResizing) return;\n // Only the primary mouse button starts a splitter drag — a right-click must\n // keep its context menu rather than being preventDefault()-ed away.\n if (e.button !== 0 && e.pointerType === 'mouse') return;\n e.preventDefault();\n isResizing = true;\n moved = false;\n startX = e.clientX;\n startWidth = panel.getBoundingClientRect().width;\n // Calculate max width dynamically based on grid container width\n maxWidth = availableMax(); // 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 cancelDrag = startPointerDrag(e, handle, {\n onMove,\n onEnd: () => {\n // `startPointerDrag` promotes immediately, so tap-vs-drag can only be\n // told apart by whether `onMove` ever ran.\n const wasTap = !moved;\n finish(!wasTap);\n if (wasTap) openSizePopover();\n },\n onCancel: () => {\n // Aborted (pointercancel / Escape) — snap back to the pre-drag width.\n panel.style.width = `${startWidth}px`;\n finish(false);\n },\n });\n };\n\n handle.addEventListener('pointerdown', onPointerDown);\n\n // Return cleanup function\n return () => {\n handle.removeEventListener('pointerdown', onPointerDown);\n cancelDrag?.();\n cancelDrag = null;\n sizePopover?.dispose();\n sizePopover = null;\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 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 Styles\n *\n * Merged from the former core partials `core/styles/shell.css` +\n * `core/styles/tool-panel.css` (extraction #370). Near-verbatim concatenation —\n * cascade layer (`@layer tbw-base`) preserved so behavior is byte-identical\n * while both copies coexist during Phase 1a. Core copies are removed in\n * Task 1b.1.5; the layer question (tbw-base vs tbw-plugins) is revisited then.\n *\n * EXCEPTION: `.tbw-scroll-area` and `.tbw-grid-content` are NOT here — both are\n * core scroll-viewport layout (rendered by dom-builder for every grid, shell or\n * not) and live in `core/styles/base.css`. Without a shell, `.tbw-grid-content`\n * still wraps the scroll area, so its layout rule must load unconditionally.\n *\n * @layer tbw-base\n */\n\n/* ==========================================================================\n * Shell Layout (was core/styles/shell.css)\n *\n * Styles for the grid shell including header, toolbar, content area,\n * and scroll container.\n * ========================================================================== */\n\n@layer tbw-base {\n tbw-grid {\n .tbw-shell-header {\n display: flex;\n align-items: center;\n gap: 8px;\n min-height: var(--tbw-shell-header-height);\n padding: 0 8px;\n background: var(--tbw-shell-header-bg);\n border-bottom: 1px solid var(--tbw-shell-header-border);\n flex-shrink: 0;\n }\n\n .tbw-shell-title {\n font-size: var(--tbw-shell-title-font-size);\n font-weight: var(--tbw-shell-title-font-weight);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n }\n\n .tbw-shell-content {\n flex: 1;\n display: flex;\n align-items: center;\n gap: 12px;\n min-width: 0;\n overflow: hidden;\n }\n\n .tbw-shell-toolbar {\n display: flex;\n align-items: center;\n gap: var(--tbw-toolbar-button-gap);\n flex-shrink: 0;\n }\n\n .tbw-toolbar-btn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: var(--tbw-toolbar-button-size);\n height: var(--tbw-toolbar-button-size);\n padding: 0;\n border: 1px solid transparent;\n border-radius: var(--tbw-border-radius);\n background: transparent;\n color: var(--tbw-color-fg);\n cursor: pointer;\n font-size: 16px;\n transition:\n background var(--tbw-transition-duration) var(--tbw-transition-ease),\n border-color var(--tbw-transition-duration) var(--tbw-transition-ease);\n\n &:hover {\n background: var(--tbw-color-row-hover);\n }\n\n &:focus-visible {\n outline: var(--tbw-focus-outline);\n outline-offset: var(--tbw-focus-outline-offset);\n }\n\n &.active {\n background: var(--tbw-focus-background);\n border-color: var(--tbw-color-accent);\n }\n\n &:disabled {\n opacity: 0.5;\n cursor: not-allowed;\n }\n }\n\n .tbw-toolbar-separator {\n width: 1px;\n height: 20px;\n background: var(--tbw-color-border);\n margin: 0 4px;\n }\n\n .tbw-shell-body {\n position: relative;\n display: flex;\n flex: 1;\n min-height: 0;\n overflow: visible;\n }\n }\n}\n\n/* ==========================================================================\n * Tool Panel (was core/styles/tool-panel.css)\n *\n * Styles for the collapsible side panel including header, content,\n * resize handle, and accordion components.\n * ========================================================================== */\n\n@layer tbw-base {\n tbw-grid {\n .tbw-tool-panel {\n position: absolute;\n top: 0;\n bottom: 0;\n right: 0;\n width: 0;\n /* `clip` rather than `hidden` so the clip region can be widened: the\n splitter's hit area straddles the docked edge and has to survive it.\n `overflow-clip-margin` is the one place `calc()` is avoided — Chromium\n drops the declaration when the value is calculated, so the overhang is\n its own token and the splitter geometry is derived from it. */\n overflow: clip;\n overflow-clip-margin: var(--tbw-tool-panel-resize-overhang, 12px);\n background: var(--tbw-tool-panel-bg);\n border-left: 1px solid var(--tbw-tool-panel-border);\n transition: width var(--tbw-tool-panel-transition);\n display: flex;\n flex-direction: column;\n z-index: var(--tbw-z-layer-toolpanel, 31);\n box-shadow: -2px 0 8px var(--tbw-color-shadow);\n\n &[data-position='left'] {\n right: auto;\n left: 0;\n border-left: none;\n border-right: 1px solid var(--tbw-tool-panel-border);\n box-shadow: 2px 0 8px var(--tbw-color-shadow);\n }\n\n &.open {\n /* Never wider than the grid it docks into. An overflowing panel is cut\n off by the clip above with no way to scroll to what is missing — the\n labels and the left half of every chip simply vanish (SC 1.4.10\n Reflow). The 20px reserve matches the drag clamp in\n `setupToolPanelResize`, so no path can reach a size the other cannot.\n `max-width` as well as `width`: a dragged width lands as an inline\n style, which only a max-width can rein back in when the grid later\n narrows under it. */\n width: min(var(--tbw-tool-panel-width), 100% - 20px);\n max-width: calc(100% - 20px);\n }\n }\n\n /* ========== Push mode ========== */\n\n /* In push mode the panel becomes a flex sibling of the grid content\n instead of overlaying it. Opening the panel shrinks the grid's\n available width; the grid's ResizeObserver re-runs column\n virtualization, so cells that would be hidden by an overlay panel\n remain reachable.\n\n The overlay shadow is dropped (nothing to lift off) and the resize\n handle is suppressed: the panel's width is part of the layout, so\n drag-to-resize would fight the flex container instead of cooperating\n with it. */\n .tbw-shell-body[data-mode='push'] {\n .tbw-tool-panel {\n position: relative;\n top: auto;\n bottom: auto;\n right: auto;\n left: auto;\n flex-shrink: 0;\n box-shadow: none;\n z-index: auto;\n /* In overlay mode the 1px side border is harmless because the panel\n floats above the grid. In push mode the panel is a flex sibling,\n so a non-zero border on a width:0 panel still reserves 1px of\n horizontal space and shows as a stray divider. Drop the border\n by default and only re-apply it on the open state. */\n border-left: none;\n border-right: none;\n\n &.open {\n border-left: 1px solid var(--tbw-tool-panel-border);\n }\n\n &[data-position='left'].open {\n border-left: none;\n border-right: 1px solid var(--tbw-tool-panel-border);\n }\n }\n\n .tbw-tool-panel-resize {\n display: none;\n }\n }\n\n /* ========== Dropdown mode ========== */\n\n /* In dropdown mode the whole tool-panel sidebar (the full accordion) is\n promoted to the top layer as a native popover and anchored near a\n trigger (the built-in toggle, a custom column-header button, or the grid\n corner). The docked absolute/full-height sidebar styling is replaced by\n a self-sized floating card with an internal scroll region.\n\n The resize handle is suppressed (a popover has no docked edge to drag)\n and the open/close width transition is dropped (the popover is shown and\n hidden via the Popover API, not by animating width). */\n .tbw-tool-panel[popover] {\n position: fixed;\n inset: auto;\n width: var(--tbw-tool-panel-width);\n max-width: calc(100vw - 16px);\n height: auto;\n max-height: min(70vh, 480px);\n overflow: hidden auto;\n margin: 4px 0 0 0;\n padding: 0;\n border: 1px solid var(--tbw-tool-panel-border);\n border-radius: var(--tbw-border-radius, 6px);\n box-shadow: 0 8px 24px var(--tbw-color-shadow);\n transition: none;\n\n /* The native popover UA rule hides a closed popover with `display: none`,\n but the base `.tbw-tool-panel { display: flex }` author rule outranks\n the UA origin and would leak the closed popover onto the page (floating\n over the header). Re-assert hidden-until-open here, and restore the\n flex layout only while the popover is actually shown. */\n display: none;\n\n /* `:popover-open` is the native (Popover API) open signal. `.open` is the\n JS-toggled fallback (`updatePanelState`) so dropdown mode still renders\n in browsers / DOMs without Popover API support, where `showPopover()`\n is unavailable and `:popover-open` never matches. */\n &:popover-open,\n &.open {\n display: flex;\n width: var(--tbw-tool-panel-width);\n }\n\n .tbw-tool-panel-resize {\n display: none;\n }\n }\n\n /* CSS anchor positioning (progressive enhancement). When the browser\n supports it, the controller wires `anchor-name` on the trigger and\n `position-anchor` on the popover; JS only sets fixed coordinates as a\n fallback for browsers without anchor positioning. */\n @supports (anchor-name: --x) {\n .tbw-tool-panel[popover] {\n /* Default (right position): align popover's right edge to the anchor's\n right edge and drop below it. */\n top: anchor(bottom);\n right: anchor(right);\n left: auto;\n position-try-fallbacks:\n flip-block,\n flip-inline,\n flip-block flip-inline;\n\n /* Grid-corner fallback anchors to the top of the grid host. */\n &[data-anchor='corner'] {\n top: anchor(top);\n }\n\n &[data-position='left'] {\n left: anchor(left);\n right: auto;\n }\n }\n }\n\n .tbw-tool-panel-resize {\n position: absolute;\n top: 0;\n bottom: 0;\n /* Transparent hit area, twice the overhang wide (24px by default, the\n SC 2.5.8 minimum) and centred on the docked edge, so half of it falls\n outside the panel and it never covers the panel's own controls. */\n width: calc(var(--tbw-tool-panel-resize-overhang, 12px) * 2);\n cursor: col-resize;\n background: transparent;\n z-index: 10;\n /* The splitter drag is driven by pointer capture, so the browser must not\n claim the gesture for panning on touch devices. */\n touch-action: none;\n user-select: none;\n\n /* The visible bar is only the 6px at the docked edge; the rest of the\n width is invisible hit area. */\n &::before {\n content: '';\n position: absolute;\n top: 0;\n bottom: 0;\n width: 6px;\n background: transparent;\n transition: background var(--tbw-transition-duration) var(--tbw-transition-ease);\n }\n\n &[data-handle-position='left'] {\n left: calc(3px - var(--tbw-tool-panel-resize-overhang, 12px));\n\n &::before {\n left: calc(var(--tbw-tool-panel-resize-overhang, 12px) - 3px);\n }\n }\n\n &[data-handle-position='right'] {\n right: calc(3px - var(--tbw-tool-panel-resize-overhang, 12px));\n\n &::before {\n right: calc(var(--tbw-tool-panel-resize-overhang, 12px) - 3px);\n }\n }\n\n /* `[data-tbw-size-open]` keeps the splitter lit while the size popover is\n open, so the popover stays visually tied to the edge it is sizing. */\n &:hover::before,\n &.resizing::before,\n &[data-tbw-size-open]::before {\n background: var(--tbw-color-accent);\n }\n }\n\n /* A closed panel is zero-width, so the splitter would hang over the grid\n inside the clip margin. It has nothing to size until the panel is open. */\n .tbw-tool-panel:not(.open) .tbw-tool-panel-resize {\n display: none;\n }\n\n .tbw-tool-panel-header {\n display: flex;\n align-items: center;\n justify-content: flex-end;\n min-height: var(--tbw-tool-panel-header-height);\n padding: 0 12px;\n border-bottom: 1px solid var(--tbw-tool-panel-border);\n flex-shrink: 0;\n }\n\n .tbw-tool-panel-title {\n font-weight: 600;\n font-size: 13px;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n }\n\n .tbw-tool-panel-close {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 24px;\n height: 24px;\n padding: 0;\n border: none;\n border-radius: var(--tbw-border-radius);\n background: transparent;\n color: var(--tbw-color-fg-muted);\n cursor: pointer;\n font-size: 14px;\n\n &:hover {\n background: var(--tbw-color-row-hover);\n color: var(--tbw-color-fg);\n }\n }\n\n /* Inline close button — shares the first accordion header row in\n single-panel mode (header bar hidden, overlay/push mode). */\n .tbw-accordion-header-row {\n display: flex;\n align-items: stretch;\n\n .tbw-accordion-header {\n flex: 1;\n min-width: 0;\n }\n }\n\n .tbw-tool-panel-close--inline {\n align-self: center;\n flex-shrink: 0;\n margin-inline-end: 8px;\n }\n\n .tbw-tool-panel-content {\n flex: 1;\n overflow: auto;\n }\n\n /* ========== Accordion ========== */\n\n .tbw-accordion {\n display: flex;\n flex-direction: column;\n gap: 0;\n }\n\n .tbw-accordion-section {\n border-bottom: 1px solid var(--tbw-tool-panel-border);\n\n &:last-child {\n border-bottom: none;\n }\n\n &.single .tbw-accordion-header {\n cursor: default;\n\n &:hover {\n background: transparent;\n }\n }\n\n &.expanded {\n .tbw-accordion-chevron {\n transform: rotate(90deg);\n }\n\n .tbw-accordion-content {\n display: block;\n }\n }\n }\n\n .tbw-accordion-header {\n display: flex;\n align-items: center;\n gap: 8px;\n width: 100%;\n padding: 10px 12px;\n border: none;\n background: transparent;\n color: var(--tbw-color-fg);\n font-size: 13px;\n font-weight: 600;\n text-align: start;\n cursor: pointer;\n user-select: none;\n\n &:hover {\n background: var(--tbw-color-row-hover);\n }\n }\n\n .tbw-accordion-chevron {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 16px;\n height: 16px;\n font-size: 10px;\n color: var(--tbw-color-fg-muted);\n transition: transform 0.15s ease;\n flex-shrink: 0;\n }\n\n .tbw-accordion-icon {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 16px;\n height: 16px;\n font-size: 14px;\n flex-shrink: 0;\n }\n\n .tbw-accordion-title {\n flex: 1;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n }\n\n .tbw-accordion-content {\n display: none;\n }\n }\n}\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')`. The equivalent `<tbw-grid>` delegate\n // methods were removed in v3 (extraction #370). Each guards on `hasState`\n // so a call 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 // `initialState` (or `locked`) is authoritative when set. Otherwise a\n // `defaultOpen` section implies the sidebar opens on load. See issue #259\n // for the proposal to split these two concerns.\n // Search marker: TOOLPANEL-OPEN-259\n const shouldOpenOnLoad =\n toolPanelCfg?.locked === true ||\n toolPanelCfg?.initialState === 'open' ||\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"],"mappings":"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"}
1
+ {"version":3,"file":"shell.umd.js","names":["button","startPointerDrag","BaseGridPlugin","styles","global","factory","exports","module","require","define","amd","globalThis","self","TbwGridPlugin_shell","TbwGrid","this","_toolbox_web_grid","Object","defineProperty","Symbol","toStringTag","value","TOOL_PANEL_NOT_FOUND","TOOL_PANEL_DUPLICATE","HEADER_CONTENT_DUPLICATE","warnDiagnostic","code","message","gridId","pluginName","console","warn","gridPrefix","toLowerCase","docsUrl","formatDiagnostic","_F","split","map","s","reverse","join","DANGEROUS_TAGS","RegExp","slice","Set","DANGEROUS_ATTR_PATTERN","URL_ATTRS","DANGEROUS_URL_PROTOCOL","setSanitizedHTML","el","html","indexOf","replaceChildren","template","document","createElement","innerHTML","sanitizeNode","content","sanitizeToFragment","textContent","root","toRemove","elements","querySelectorAll","tagName","has","push","namespaceURI","Array","from","attributes","some","attr","test","name","attrsToRemove","attrName","forEach","removeAttribute","remove","supportsPopover","HTMLElement","prototype","showPopover","button$1","label","text","type","className","setAttribute","createSizePopover","options","step","target","input","anchorEl","opened","disposed","abort","AbortController","close","hidePopover","hidden","sync","String","Math","round","getSize","apply","size","Number","isFinite","setSize","addEventListener","e","composedPath","includes","capture","signal","key","element","open","next","anchor","id","minus","plus","reset","field","min","valueAsNumber","relatedTarget","Node","contains","append","build","host","getHost","body","parentNode","appendChild","getLabel","a","getBoundingClientRect","box","clamp","v","max","left","width","window","innerWidth","below","bottom","above","top","height","tall","innerHeight","style","place","focus","dispose","GridClasses","applyAttrs","attrs","tag","children","child","createTextNode","div","button","gridContentTemplate","cloneGridContent","cloneNode","buildGridDOM","fragment","createDocumentFragment","hasShell","shellBody","shellHeader","contentWrapper","buildShellHeader","header","part","role","title","titleEl","toolbar","btn","configButtons","hasRender","apiButtons","hasCustomContent","b","showToggle","hasPanels","toggleBtn","isPanelOpen","dataset","icon","toolPanelIcon","buildPanelCloseButton","inline","closeBtn","buildPanelCloseRow","panelHeader","buildAccordionSection","panel","isSinglePanel","inlineClose","renderHeader","canInlineClose","section","isExpanded","headerBtn","iconSpan","class","titleSpan","chevronSpan","expandIcon","buildAccordionHeader","headerRow","buildShellBody","mode","gridContent","panelEl","panels","length","position","popover","tabindex","showCloseButton","panelContent","accordion","buildToolPanel","iconToString","sanitizeHTML","outerHTML","shouldRenderShellHeader","config","toolbarContents","toolPanels","headerContents","lightDomContent","hasToolButtonsContainer","shouldRenderHeaderBar","visible","isInGrid","node","gridElement","getRootNode","ShadowRoot","setupToolPanelResize","renderRoot","onResize","querySelector","handle","toolPanel","startX","startWidth","maxWidth","isResizing","moved","cancelDrag","sizePopover","availableMax","onMove","preventDefault","delta","clientX","newWidth","finish","commit","classList","transition","cursor","userSelect","finalWidth","onPointerDown","pointerType","add","startPointerDrag","onEnd","wasTap","clamped","closest","onCancel","removeEventListener","renderHeaderContent","state","hasLightDomContent","lightDomHeaderContent","lightDomContentMoved","hasPluginContent","contentArea","display","sortedContents","values","sort","order","existingCleanup","headerContentCleanups","get","delete","container","cleanup","render","set","renderPanelContent","_icons","panelId","expandedSections","toggle","panelCleanups","updateToolbarActiveStates","panelToggle","updatePanelState","DROPDOWN_ANCHOR_NAME","dropdownAnchorSeq","supportsAnchorPositioning","CSS","supports","showToolPanelDropdown","corner","dropdownAnchorEl","hasAttribute","anchorName","dropdownAnchorName","setProperty","removeProperty","rect","getAttribute","right","viewportWidth","positionDropdownFallback","prepareForRerender","toolbarContentCleanups","clear","buildShellOptions","shellConfig","runtimeState","icons","expand","collapseIcon","collapse","sortedPanels","headerOptions","toolPanelToggle","locked","c","hasElement","bodyOptions","p","rebuildShellDOM","existingRoot","existingContent","preservedNodes","n","firstChild","nextSibling","nodeType","buildGridDOMIntoElement","freshContent","replaceWith","createShellController","grid","initialized","controller","isInitialized","setInitialized","openToolPanel","targetSectionId","toggleToolPanelSection","defaultOpenId","effectiveConfig","shell","defaultOpen","firstPanel","shadow","_renderRoot","_accordionIcons","explicit","_hostElement","resolveDropdownAnchor","_emit","sections","closeToolPanel","onClose","hideToolPanelDropdown","toggleToolPanel","reanchorOpenDropdown","stored","isConnected","getPropertyValue","fresh","trigger","prev","repairDropdownAnchor","sectionId","updateAccordionSectionState","otherId","otherPanel","contentEl","renderAccordionSectionContent","expanded","getToolPanels","registerToolPanel","refreshShellHeader","unregisterToolPanel","getHeaderContents","registerHeaderContent","unregisterHeaderContent","contentId","onDestroy","getToolbarContents","registerToolbarContent","unregisterToolbarContent","CONFIGURED_PANEL_WIDTH","for","ShellPlugin","BaseGridPlugin","styles","resizeCleanup","clickOutsideCleanup","escapeDismissCleanup","shellChangeHandler","reanchorObserver","reanchorObserving","hasState","ensureState","Map","lightDomTitle","lightDomToolPanelIds","adapterBoundToolPanelIds","lightDomToolbarContentIds","apiToolPanelIds","apiHeaderContentIds","shellState","shellController","isToolPanelOpen","expandedToolPanelSections","attach","super","handler","syncLightDom","_registerLightDomHandler","parseLightDom","headerEl","parseLightDomShell","rendererFactory","toolButtonsContainer","contentDef","parseLightDomToolButtons","tooltip","parseInt","adapterRenderer","rawContent","trim","wrapper","existingPanel","firstAdapterAttach","attrsChanged","parseLightDomToolPanels","_getToolPanelRendererFactory","hadTitle","hadToolButtons","hasTitle","hasToolButtons","_requestConfigMerge","afterStructuralRender","beforeStructuralRender","needsStructuralRerender","hadShell","hadToolPanel","accordionSectionsBefore","prevPosition","prevMode","syncPluginContributions","keys","_pluginShellContributions","processConfig","ctor","resolvedConfig","contents","apiContents","originalConfigContents","_sourceConfig","configIds","mergedContents","syncConfiguredPanelWidth","setupListeners","renderShellContents","afterRender","syncShellHeaderTitle","startReanchorObserver","stopReanchorObserver","insertBefore","previous","MutationObserver","observe","childList","subtree","disconnect","configContents","stateContents","allContents","slot","renderCustomToolbarContents","toolPanelCfg","shouldOpenOnLoad","initialState","callbacks","onPanelToggle","onPanelClose","onSectionToggle","setupShellEventListeners","closeOnClickOutside","Element","matches","setupClickOutsideDismiss","defaultPrevented","setupEscapeDismiss","teardownListeners","detach","disposeShellState","_unregisterLightDomHandler","cleanupShellState"],"sources":["../../../../../libs/grid/src/lib/core/internal/diagnostics.ts","../../../../../libs/grid/src/lib/core/internal/sanitize.ts","../../../../../libs/grid/src/lib/core/internal/size-popover.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/shell.css?inline","../../../../../libs/grid/src/lib/plugins/shell/shell-plugin.ts"],"sourcesContent":["/**\n * Centralized diagnostic messages for @toolbox-web/grid.\n *\n * Every user-facing warning, error, or info message in the grid has a unique\n * diagnostic code (e.g. `TBW001`). Each code maps to a section on the online\n * troubleshooting page, giving developers a direct link to resolution steps.\n *\n * ## Usage\n *\n * ```ts\n * import { MISSING_BREAKPOINT, warnDiagnostic, throwDiagnostic } from './diagnostics';\n *\n * // Warn with a code\n * warnDiagnostic(MISSING_BREAKPOINT, 'Set a breakpoint...', gridId);\n *\n * // Throw with a code\n * throwDiagnostic(MISSING_ROW_ID, 'Configure getRowId...', gridId);\n * ```\n *\n * Plugins should prefer `this.warn(MISSING_BREAKPOINT, message)` via BaseGridPlugin\n * instead of importing this module directly.\n *\n * @internal\n */\n\n// #region Grid Prefix\n\n/**\n * Build the `[tbw-grid]` or `[tbw-grid#my-id]` log prefix.\n * Pass `pluginName` for a scoped prefix like `[tbw-grid:SelectionPlugin]`.\n */\nexport function gridPrefix(gridId?: string, pluginName?: string): string {\n const id = gridId ? `#${gridId}` : '';\n const plugin = pluginName ? `:${pluginName}` : '';\n return `[tbw-grid${id}${plugin}]`;\n}\n\n// #endregion\n\n// #region Diagnostic Codes\n\n/**\n * Diagnostic codes used across the grid library.\n *\n * Each code is an individual export so that unused codes are tree-shaken\n * from bundles that don't reference them (esbuild can't tree-shake\n * properties from a single object).\n *\n * Naming: TBW + 3-digit number.\n * Ranges:\n * 001–019 Configuration validation (missing plugins, bad config)\n * 020–029 Plugin lifecycle (dependencies, incompatibilities, alias collapse)\n * 030–039 Feature registry\n * 040–049 Row operations (row ID, row mutations)\n * 050–059 Column operations (width, template)\n * 060–069 Rendering (callbacks, formatters, external views)\n * 070–079 Shell (tool panels, header/toolbar content)\n * 080–089 Editing & editors\n * 090–099 Print\n * 100–109 Clipboard\n * 110–119 Plugin-specific (responsive, undo-redo, grouping-columns)\n * 120–129 Style injection\n * 130–139 Attribute parsing\n * 140–149 DataSource / ServerSide\n */\n\n// --- Config validation (001–019) ---\n/** Column uses a plugin-owned property but the plugin is not loaded. */\nexport const MISSING_PLUGIN = 'TBW001' as const;\n/** Grid config uses a plugin-owned property but the plugin is not loaded. */\nexport const MISSING_PLUGIN_CONFIG = 'TBW002' as const;\n/** Plugin config rule violation (error severity). */\nexport const CONFIG_RULE_ERROR = 'TBW003' as const;\n/** Plugin config rule violation (warning severity). */\nexport const CONFIG_RULE_WARN = 'TBW004' as const;\n\n// --- Plugin lifecycle (020–029) ---\n/** Required plugin dependency is missing. */\nexport const MISSING_DEPENDENCY = 'TBW020' as const;\n/** Optional plugin dependency is missing. */\nexport const OPTIONAL_DEPENDENCY = 'TBW021' as const;\n/** Two loaded plugins are incompatible. */\nexport const INCOMPATIBLE_PLUGINS = 'TBW022' as const;\n/** Two plugin instances resolve to the same canonical name (alias collapse). */\nexport const PLUGIN_ALIAS_COLLAPSE = 'TBW023' as const;\n/** Error thrown inside a plugin event handler. */\nexport const PLUGIN_EVENT_ERROR = 'TBW024' as const;\n/** Conflicting config values found while merging alias-collapsed plugin instances. */\nexport const PLUGIN_ALIAS_CONFIG_CONFLICT = 'TBW025' as const;\n\n// --- Feature registry (030–039) ---\n/** Feature was re-registered (overwritten). */\nexport const FEATURE_REREGISTERED = 'TBW030' as const;\n/** Feature configured but not imported. */\nexport const FEATURE_NOT_IMPORTED = 'TBW031' as const;\n/** Feature depends on another feature that is not enabled. */\nexport const FEATURE_MISSING_DEP = 'TBW032' as const;\n\n// --- Row operations (040–049) ---\n/** Cannot determine row ID (no getRowId and no id property). */\nexport const MISSING_ROW_ID = 'TBW040' as const;\n/** Row with given ID not found. */\nexport const ROW_NOT_FOUND = 'TBW041' as const;\n\n// --- Column operations (050–059) ---\n/** Column has an invalid CSS width value. */\nexport const INVALID_COLUMN_WIDTH = 'TBW050' as const;\n\n// --- Rendering callbacks (060–069) ---\n/** rowClass callback threw an error. */\nexport const ROW_CLASS_ERROR = 'TBW060' as const;\n/** cellClass callback threw an error. */\nexport const CELL_CLASS_ERROR = 'TBW061' as const;\n/** Column format function threw an error. */\nexport const FORMAT_ERROR = 'TBW062' as const;\n/** External view mount() threw an error. */\nexport const VIEW_MOUNT_ERROR = 'TBW063' as const;\n/** External view event dispatch error. */\nexport const VIEW_DISPATCH_ERROR = 'TBW064' as const;\n\n// --- Shell (070–079) ---\n/** Tool panel missing required id. */\nexport const TOOL_PANEL_MISSING_ATTR = 'TBW070' as const;\n/** No tool panels registered. */\nexport const NO_TOOL_PANELS = 'TBW071' as const;\n/** Tool panel section not found. */\nexport const TOOL_PANEL_NOT_FOUND = 'TBW072' as const;\n/** Tool panel already registered. */\nexport const TOOL_PANEL_DUPLICATE = 'TBW073' as const;\n/** Header content already registered. */\nexport const HEADER_CONTENT_DUPLICATE = 'TBW074' as const;\n/** Toolbar content already registered. */\nexport const TOOLBAR_CONTENT_DUPLICATE = 'TBW075' as const;\n\n// --- Editing & editors (080–089) ---\n/** External editor mount() threw an error. */\nexport const EDITOR_MOUNT_ERROR = 'TBW080' as const;\n\n// --- Print (090–099) ---\n/** Print already in progress. */\nexport const PRINT_IN_PROGRESS = 'TBW090' as const;\n/** Grid not available for printing. */\nexport const PRINT_NO_GRID = 'TBW091' as const;\n/** Print operation failed. */\nexport const PRINT_FAILED = 'TBW092' as const;\n/** Multiple elements share the same grid ID (print isolation issue). */\nexport const PRINT_DUPLICATE_ID = 'TBW093' as const;\n\n// --- Clipboard (100–109) ---\n/** Clipboard API write failed. */\nexport const CLIPBOARD_FAILED = 'TBW100' as const;\n\n// --- Plugin-specific (110–119) ---\n/** ResponsivePlugin: no breakpoint configured. */\nexport const MISSING_BREAKPOINT = 'TBW110' as const;\n/** UndoRedoPlugin: transaction already in progress. */\nexport const TRANSACTION_IN_PROGRESS = 'TBW111' as const;\n/** UndoRedoPlugin: no transaction in progress. */\nexport const NO_TRANSACTION = 'TBW112' as const;\n/** GroupingColumnsPlugin: missing id or header on column group definition. */\nexport const COLUMN_GROUP_NO_ID = 'TBW113' as const;\n/** GroupingColumnsPlugin: conflicting columnGroups sources. */\nexport const COLUMN_GROUPS_CONFLICT = 'TBW114' as const;\n\n// --- Style injection (120–129) ---\n/** Failed to extract grid.css from document stylesheets. */\nexport const STYLE_EXTRACT_FAILED = 'TBW120' as const;\n/** Could not find grid.css in document.styleSheets. */\nexport const STYLE_NOT_FOUND = 'TBW121' as const;\n\n// --- Attribute parsing (130–139) ---\n/** Invalid JSON in an HTML attribute. */\nexport const INVALID_ATTRIBUTE_JSON = 'TBW130' as const;\n\n// --- DataSource / ServerSide (140–149) ---\n/** ServerSidePlugin: getRows() rejected. */\nexport const DATASOURCE_FETCH_ERROR = 'TBW140' as const;\n/** ServerSidePlugin: getChildRows() rejected. */\nexport const DATASOURCE_CHILD_FETCH_ERROR = 'TBW141' as const;\n/** ServerSidePlugin: getChildRows() not implemented but a plugin queried for children. */\nexport const DATASOURCE_NO_CHILD_HANDLER = 'TBW142' as const;\n/** ServerSidePlugin: concurrent request limit reached, request deferred. */\nexport const DATASOURCE_THROTTLED = 'TBW143' as const;\n\nexport type DiagnosticCode =\n | typeof MISSING_PLUGIN\n | typeof MISSING_PLUGIN_CONFIG\n | typeof CONFIG_RULE_ERROR\n | typeof CONFIG_RULE_WARN\n | typeof MISSING_DEPENDENCY\n | typeof OPTIONAL_DEPENDENCY\n | typeof INCOMPATIBLE_PLUGINS\n | typeof PLUGIN_ALIAS_COLLAPSE\n | typeof PLUGIN_EVENT_ERROR\n | typeof PLUGIN_ALIAS_CONFIG_CONFLICT\n | typeof FEATURE_REREGISTERED\n | typeof FEATURE_NOT_IMPORTED\n | typeof FEATURE_MISSING_DEP\n | typeof MISSING_ROW_ID\n | typeof ROW_NOT_FOUND\n | typeof INVALID_COLUMN_WIDTH\n | typeof ROW_CLASS_ERROR\n | typeof CELL_CLASS_ERROR\n | typeof FORMAT_ERROR\n | typeof VIEW_MOUNT_ERROR\n | typeof VIEW_DISPATCH_ERROR\n | typeof TOOL_PANEL_MISSING_ATTR\n | typeof NO_TOOL_PANELS\n | typeof TOOL_PANEL_NOT_FOUND\n | typeof TOOL_PANEL_DUPLICATE\n | typeof HEADER_CONTENT_DUPLICATE\n | typeof TOOLBAR_CONTENT_DUPLICATE\n | typeof EDITOR_MOUNT_ERROR\n | typeof PRINT_IN_PROGRESS\n | typeof PRINT_NO_GRID\n | typeof PRINT_FAILED\n | typeof PRINT_DUPLICATE_ID\n | typeof CLIPBOARD_FAILED\n | typeof MISSING_BREAKPOINT\n | typeof TRANSACTION_IN_PROGRESS\n | typeof NO_TRANSACTION\n | typeof COLUMN_GROUP_NO_ID\n | typeof COLUMN_GROUPS_CONFLICT\n | typeof STYLE_EXTRACT_FAILED\n | typeof STYLE_NOT_FOUND\n | typeof INVALID_ATTRIBUTE_JSON\n | typeof DATASOURCE_FETCH_ERROR\n | typeof DATASOURCE_CHILD_FETCH_ERROR\n | typeof DATASOURCE_NO_CHILD_HANDLER\n | typeof DATASOURCE_THROTTLED;\n\n// #endregion\n\n// #region Docs URL\n\nconst DOCS_BASE = 'https://toolboxjs.com/grid/errors';\n\n/** Build a direct link to the troubleshooting section for a code. */\nfunction docsUrl(code: DiagnosticCode): string {\n return `${DOCS_BASE}#${code.toLowerCase()}`;\n}\n\n// #endregion\n\n// #region Formatting\n\n/**\n * Format a diagnostic message with prefix, code, and docs link.\n *\n * Output format:\n * ```\n * [tbw-grid#my-id] TBW001: Your message here.\n *\n * → More info: https://toolboxjs.com/grid/errors#tbw001\n * ```\n */\nexport function formatDiagnostic(code: DiagnosticCode, message: string, gridId?: string, pluginName?: string): string {\n const prefix = gridPrefix(gridId, pluginName);\n return `${prefix} ${code}: ${message}\\n\\n → More info: ${docsUrl(code)}`;\n}\n\n// #endregion\n\n// #region Public API\n\n/**\n * Throw an error with a diagnostic code and docs link.\n * Use for configuration errors and API misuse that should halt execution.\n */\nexport function throwDiagnostic(code: DiagnosticCode, message: string, gridId?: string, pluginName?: string): never {\n throw new Error(formatDiagnostic(code, message, gridId, pluginName));\n}\n\n/**\n * Log a warning with a diagnostic code and docs link.\n * Use for recoverable issues the developer should fix.\n */\nexport function warnDiagnostic(code: DiagnosticCode, message: string, gridId?: string, pluginName?: string): void {\n console.warn(formatDiagnostic(code, message, gridId, pluginName));\n}\n\n/**\n * Log a debug message with a diagnostic code and docs link.\n * Use for optional/soft dependency notifications — visible only when\n * the browser DevTools \"Verbose\" log level is enabled.\n */\nexport function debugDiagnostic(code: DiagnosticCode, message: string, gridId?: string, pluginName?: string): void {\n console.debug(formatDiagnostic(code, message, gridId, pluginName));\n}\n\n/**\n * Log an error with a diagnostic code and docs link.\n * Use for non-throwing errors (e.g., failed async operations).\n */\nexport function errorDiagnostic(code: DiagnosticCode, message: string, gridId?: string, pluginName?: string): void {\n console.error(formatDiagnostic(code, message, gridId, pluginName));\n}\n\n// #endregion\n","// Centralized template expression evaluation & sanitization utilities.\n// Responsible for safely interpolating {{ }} expressions while blocking\n// access to dangerous globals / reflective capabilities.\nimport type { CompiledViewFunction, EvalContext } from '../types';\n\n// #region Constants\nconst EXPR_RE = /{{\\s*([^}]+)\\s*}}/g;\nconst EMPTY_SENTINEL = '__DG_EMPTY__';\nconst SAFE_EXPR = /^[\\w$. '?+\\-*/%:()!<>=,&|]+$/;\n\n// Forbidden identifiers built at runtime to avoid static analysis scanners\n// from flagging string literals (e.g. socket.dev \"network access\" / \"uses eval\").\n// Stored as reversed, pipe-delimited tokens — reversed back when constructing the regex.\nconst _F =\n '__otorp__|__retteGenifed__|__retteSenifed__|rotcurtsnoc|wodniw|sihTlabolg|labolg|ssecorp|noitcnuF|tropmi|lave|tcelfeR|yxorP|rorrE|stnemugra|tnemucod|noitacol|eikooc|egarotSlacol|egarotSnoisses|BDdexedni|hctef|tseuqeRpttHLMX|tekcoSbeW|rekroW|rekroWderahS|rekroWecivreS|renepo|tnerap|pot|semarf|fles'\n .split('|')\n .map((s) => s.split('').reverse().join(''));\nconst FORBIDDEN = new RegExp(`__(proto|defineGetter|defineSetter)|${_F.slice(3).join('|')}|this\\\\b`);\n// #endregion\n\n// #region HTML Sanitization\n\n/**\n * Escape a plain text string for safe insertion into HTML.\n * Converts special HTML characters to their entity equivalents.\n *\n * @param text - Plain text string to escape\n * @returns HTML-safe string\n */\nexport function escapeHtml(text: string): string {\n if (!text || typeof text !== 'string') return '';\n return text\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;')\n .replace(/'/g, '&#39;');\n}\n\n/**\n * Tags that are considered dangerous and will be completely removed.\n * These can execute scripts, load external resources, or manipulate the page.\n */\nconst DANGEROUS_TAGS = new Set(\n 'script|iframe|object|embed|form|input|button|textarea|select|link|meta|base|style|template|slot|portal|frame|frameset|applet|noscript|noembed|plaintext|xmp|listing|math'.split(\n '|',\n ),\n);\n\n/**\n * Attributes that are considered dangerous - event handlers and data loading.\n */\nconst DANGEROUS_ATTR_PATTERN = /^on\\w+$/i;\n\n/**\n * Attributes that can contain URLs which might be javascript: or data: URIs.\n */\nconst URL_ATTRS = new Set('href|src|action|formaction|data|srcdoc|xlink:href|poster|srcset'.split('|'));\n\n/**\n * Protocol patterns that are dangerous in URLs.\n */\nconst DANGEROUS_URL_PROTOCOL = /^\\s*(javascript|vbscript|data|blob):/i;\n\n/**\n * Sanitize an HTML string by removing dangerous tags and attributes.\n *\n * Prefer {@link setSanitizedHTML} wherever the result is destined for an\n * element: this function has to re-serialize the sanitized tree back to a\n * string, and the caller's `innerHTML =` then re-parses it. That\n * parse→serialize→parse round trip is the classic mutation-XSS vector, and it\n * costs a second parse. Use this overload only when a string is genuinely\n * required (e.g. the value is stored, compared, or handed to another API).\n *\n * @param html - Raw HTML string to sanitize\n * @returns Sanitized HTML string safe for innerHTML\n */\nexport function sanitizeHTML(html: string): string {\n if (!html || typeof html !== 'string') return '';\n\n // Fast path: if no HTML tags at all, return as-is (already safe)\n if (html.indexOf('<') === -1) return html;\n\n const template = document.createElement('template');\n template.innerHTML = html;\n\n sanitizeNode(template.content);\n\n return template.innerHTML;\n}\n\n/**\n * Parse and sanitize an HTML string into a detached {@link DocumentFragment}.\n *\n * This is the primitive {@link setSanitizedHTML} builds on, exposed for callers\n * that need to inspect or relocate the nodes before insertion.\n *\n * @param html - Raw HTML string to sanitize\n * @returns Fragment containing the sanitized nodes (empty for falsy input)\n */\nexport function sanitizeToFragment(html: string): DocumentFragment {\n const template = document.createElement('template');\n if (html && typeof html === 'string') {\n template.innerHTML = html;\n sanitizeNode(template.content);\n }\n return template.content;\n}\n\n/**\n * Replace an element's children with the sanitized parse of `html`.\n *\n * **This is the canonical sink for user-supplied markup** — cell/header/group\n * renderer string returns, `gridConfig.icons.*`, light-DOM tool panel fallback\n * content, and any new extension point that turns a user string into markup.\n * `textContent` is wrong here (it renders SVG icon strings as literal text) and\n * raw `innerHTML` is wrong (it is an XSS sink).\n *\n * Nodes are moved from the parse fragment rather than re-serialized, so the\n * sanitized tree is exactly what lands in the document.\n *\n * @param el - Element whose children are replaced\n * @param html - Raw HTML string to sanitize and insert\n */\nexport function setSanitizedHTML(el: Element, html: string): void {\n if (!html || typeof html !== 'string') {\n el.replaceChildren();\n return;\n }\n // Nothing for the parser to do: no markup and no entities to decode.\n if (html.indexOf('<') === -1 && html.indexOf('&') === -1) {\n el.textContent = html;\n return;\n }\n el.replaceChildren(sanitizeToFragment(html));\n}\n\n/**\n * Recursively sanitize a DOM node tree.\n */\nfunction sanitizeNode(root: DocumentFragment | Element): void {\n const toRemove: Element[] = [];\n\n // Use querySelectorAll to find all elements, then filter\n const elements = root.querySelectorAll('*');\n\n for (const el of elements) {\n const tagName = el.tagName.toLowerCase();\n\n // Check if tag is dangerous\n if (DANGEROUS_TAGS.has(tagName)) {\n toRemove.push(el);\n continue;\n }\n\n // SVG elements need special handling - they can contain script-like behavior\n if (tagName === 'svg' || el.namespaceURI === 'http://www.w3.org/2000/svg') {\n // Remove entire SVG if it has any suspicious attributes\n const hasDangerousContent = Array.from(el.attributes).some(\n (attr) => DANGEROUS_ATTR_PATTERN.test(attr.name) || attr.name === 'href' || attr.name === 'xlink:href',\n );\n if (hasDangerousContent) {\n toRemove.push(el);\n continue;\n }\n }\n\n // Check and remove dangerous attributes\n const attrsToRemove: string[] = [];\n for (const attr of el.attributes) {\n const attrName = attr.name.toLowerCase();\n\n // Event handlers (onclick, onerror, onload, etc.)\n if (DANGEROUS_ATTR_PATTERN.test(attrName)) {\n attrsToRemove.push(attr.name);\n continue;\n }\n\n // `is=` upgrades a plain tag into a registered customized built-in, so it\n // can smuggle behaviour past the tag-name check above.\n if (attrName === 'is') {\n attrsToRemove.push(attr.name);\n continue;\n }\n\n // URL attributes with dangerous protocols\n if (URL_ATTRS.has(attrName) && DANGEROUS_URL_PROTOCOL.test(attr.value)) {\n attrsToRemove.push(attr.name);\n continue;\n }\n\n // style attribute can contain expressions (IE) or url() with javascript:\n if (attrName === 'style' && /expression\\s*\\(|javascript:|behavior\\s*:/i.test(attr.value)) {\n attrsToRemove.push(attr.name);\n continue;\n }\n }\n\n attrsToRemove.forEach((name) => el.removeAttribute(name));\n }\n\n // Remove dangerous elements (do this after iteration to avoid modifying during traversal)\n toRemove.forEach((el) => el.remove());\n}\n\n// #endregion\n\n// #region Template Evaluation\n\n// #region Safe Expression Evaluator\n// A minimal recursive-descent evaluator for the token set allowed by SAFE_EXPR.\n// Replaces `new Function()` to avoid dynamic code execution warnings from\n// static analysis tools (socket.dev, npm audit) while maintaining identical\n// functionality for expressions that pass the allowlist guards.\n//\n// Supported grammar (matching SAFE_EXPR = /^[\\w$. '?+\\-*/%:()!<>=,&|]+$/):\n// ternary → logicalOr ('?' ternary ':' ternary)?\n// logicalOr → logicalAnd ('||' logicalAnd)*\n// logicalAnd → equality ('&&' equality)*\n// equality → comparison (('==' | '!=' | '===' | '!==') comparison)*\n// comparison → additive (('<' | '>' | '<=' | '>=') additive)*\n// additive → multiplicative (('+' | '-') multiplicative)*\n// multiplicative → unary (('*' | '/' | '%') unary)*\n// unary → '!' unary | primary\n// primary → NUMBER | STRING | IDENT ('.' IDENT)? | '(' ternary ')'\n\ntype Token =\n { type: 'num'; v: number } | { type: 'str'; v: string } | { type: 'id'; v: string } | { type: 'op'; v: string };\n\nfunction tokenize(expr: string): Token[] | null {\n const tokens: Token[] = [];\n let i = 0;\n while (i < expr.length) {\n const ch = expr[i];\n if (ch === ' ') {\n i++;\n continue;\n }\n\n // Numbers\n if ((ch >= '0' && ch <= '9') || (ch === '.' && i + 1 < expr.length && expr[i + 1] >= '0' && expr[i + 1] <= '9')) {\n const start = i;\n while (i < expr.length && ((expr[i] >= '0' && expr[i] <= '9') || expr[i] === '.')) i++;\n tokens.push({ type: 'num', v: Number(expr.slice(start, i)) });\n continue;\n }\n\n // String literals (single-quoted only, per SAFE_EXPR)\n if (ch === \"'\") {\n let s = '';\n i++; // skip opening quote\n while (i < expr.length && expr[i] !== \"'\") s += expr[i++];\n if (i >= expr.length) return null; // unterminated\n i++; // skip closing quote\n tokens.push({ type: 'str', v: s });\n continue;\n }\n\n // Identifiers (value, row, row.field)\n if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || ch === '_' || ch === '$') {\n const start = i;\n while (i < expr.length && /[\\w$.]/.test(expr[i])) i++;\n tokens.push({ type: 'id', v: expr.slice(start, i) });\n continue;\n }\n\n // Multi-char operators\n if (i + 2 < expr.length) {\n const tri = expr.slice(i, i + 3);\n if (tri === '===' || tri === '!==') {\n tokens.push({ type: 'op', v: tri });\n i += 3;\n continue;\n }\n }\n if (i + 1 < expr.length) {\n const bi = expr.slice(i, i + 2);\n if (bi === '==' || bi === '!=' || bi === '<=' || bi === '>=' || bi === '&&' || bi === '||') {\n tokens.push({ type: 'op', v: bi });\n i += 2;\n continue;\n }\n }\n\n // Single-char operators\n if ('+-*/%<>!?:(),'.includes(ch)) {\n tokens.push({ type: 'op', v: ch });\n i++;\n continue;\n }\n\n return null; // unexpected character\n }\n return tokens;\n}\n\nfunction safeEval(expr: string, ctx: EvalContext): unknown {\n const toks = tokenize(expr);\n if (!toks) return undefined;\n const tokens: Token[] = toks;\n let pos = 0;\n\n function peek(): Token | undefined {\n return tokens[pos];\n }\n function advance(): Token {\n return tokens[pos++];\n }\n function match(type: string, val?: string): boolean {\n const t = peek();\n if (!t) return false;\n if (t.type !== type) return false;\n if (val !== undefined && t.v !== val) return false;\n return true;\n }\n\n function resolveId(name: string): unknown {\n if (name === 'value') return ctx.value;\n if (name.startsWith('row.')) {\n const key = name.slice(4);\n return ctx.row ? ctx.row[key] : undefined;\n }\n if (name.startsWith('typeDefault.')) {\n const key = name.slice('typeDefault.'.length);\n return ctx.typeDefault ? ctx.typeDefault[key] : undefined;\n }\n // bare 'row' reference\n if (name === 'row') return ctx.row;\n if (name === 'typeDefault') return ctx.typeDefault;\n return undefined;\n }\n\n function parseTernary(): unknown {\n const result = parseLogicalOr();\n if (match('op', '?')) {\n advance(); // consume '?'\n const consequent = parseTernary();\n if (!match('op', ':')) return undefined;\n advance(); // consume ':'\n const alternate = parseTernary();\n return result ? consequent : alternate;\n }\n return result;\n }\n\n function parseLogicalOr(): unknown {\n let left = parseLogicalAnd();\n while (match('op', '||')) {\n advance();\n left = left || parseLogicalAnd();\n }\n return left;\n }\n\n function parseLogicalAnd(): unknown {\n let left = parseEquality();\n while (match('op', '&&')) {\n advance();\n left = left && parseEquality();\n }\n return left;\n }\n\n function parseEquality(): unknown {\n let left = parseComparison();\n while (true) {\n const t = peek();\n if (!t || t.type !== 'op') break;\n if (t.v === '===') {\n advance();\n left = left === parseComparison();\n } else if (t.v === '!==') {\n advance();\n left = left !== parseComparison();\n } else if (t.v === '==') {\n advance();\n left = left == parseComparison();\n } else if (t.v === '!=') {\n advance();\n left = left != parseComparison();\n } else break;\n }\n return left;\n }\n\n function parseComparison(): unknown {\n let left = parseAdditive();\n while (true) {\n const t = peek();\n if (!t || t.type !== 'op') break;\n if (t.v === '<') {\n advance();\n left = (left as number) < (parseAdditive() as number);\n } else if (t.v === '>') {\n advance();\n left = (left as number) > (parseAdditive() as number);\n } else if (t.v === '<=') {\n advance();\n left = (left as number) <= (parseAdditive() as number);\n } else if (t.v === '>=') {\n advance();\n left = (left as number) >= (parseAdditive() as number);\n } else break;\n }\n return left;\n }\n\n function parseAdditive(): unknown {\n let left = parseMultiplicative();\n while (true) {\n const t = peek();\n if (!t || t.type !== 'op') break;\n if (t.v === '+') {\n advance();\n left = (left as number) + (parseMultiplicative() as number);\n } else if (t.v === '-') {\n advance();\n left = (left as number) - (parseMultiplicative() as number);\n } else break;\n }\n return left;\n }\n\n function parseMultiplicative(): unknown {\n let left = parseUnary();\n while (true) {\n const t = peek();\n if (!t || t.type !== 'op') break;\n if (t.v === '*') {\n advance();\n left = (left as number) * (parseUnary() as number);\n } else if (t.v === '/') {\n advance();\n left = (left as number) / (parseUnary() as number);\n } else if (t.v === '%') {\n advance();\n left = (left as number) % (parseUnary() as number);\n } else break;\n }\n return left;\n }\n\n function parseUnary(): unknown {\n if (match('op', '!')) {\n advance();\n return !parseUnary();\n }\n return parsePrimary();\n }\n\n function parsePrimary(): unknown {\n const t = peek();\n if (!t) return undefined;\n if (t.type === 'num') {\n advance();\n return t.v;\n }\n if (t.type === 'str') {\n advance();\n return t.v;\n }\n if (t.type === 'id') {\n advance();\n return resolveId(t.v as string);\n }\n if (t.type === 'op' && t.v === '(') {\n advance(); // consume '('\n const result = parseTernary();\n if (match('op', ')')) advance(); // consume ')'\n return result;\n }\n // Unary minus for negative numbers\n if (t.type === 'op' && t.v === '-') {\n advance();\n return -(parsePrimary() as number);\n }\n return undefined;\n }\n\n const result = parseTernary();\n // Ensure all tokens were consumed\n if (pos < tokens.length) return undefined;\n return result;\n}\n// #endregion\n\nexport function evalTemplateString(raw: string, ctx: EvalContext): string {\n if (!raw || raw.indexOf('{{') === -1) return raw; // fast path (no expressions)\n const parts: { expr: string; result: string }[] = [];\n const evaluated = raw.replace(EXPR_RE, (_m, expr) => {\n const res = evalSingle(expr, ctx);\n parts.push({ expr: expr.trim(), result: res });\n // The template markup is author-controlled; the interpolated data is not.\n // Escaping here stops row data from injecting elements or breaking out of\n // an attribute (sanitizeHTML downstream only strips *dangerous* markup).\n return res === EMPTY_SENTINEL ? res : escapeHtml(res);\n });\n const finalStr = postProcess(evaluated);\n // If every part evaluated to EMPTY_SENTINEL we treat this as intentionally blank.\n // If any expression was blocked due to forbidden token (EMPTY_SENTINEL) we *still* only output ''\n // but do not escalate to BLOCKED_SENTINEL unless the original contained explicit forbidden tokens.\n const allEmpty = parts.length && parts.every((p) => p.result === '' || p.result === EMPTY_SENTINEL);\n const hadForbidden = REFLECTIVE_RE.test(raw);\n if (hadForbidden || allEmpty) return '';\n return finalStr;\n}\n\nfunction evalSingle(expr: string, ctx: EvalContext): string {\n expr = (expr || '').trim();\n if (!expr) return EMPTY_SENTINEL;\n if (REFLECTIVE_RE.test(expr)) return EMPTY_SENTINEL;\n if (expr === 'value') return ctx.value == null ? EMPTY_SENTINEL : String(ctx.value);\n if (expr.startsWith('row.') && !/[()?]/.test(expr) && !expr.includes(':')) {\n const key = expr.slice(4);\n const v = ctx.row ? ctx.row[key] : undefined;\n return v == null ? EMPTY_SENTINEL : String(v);\n }\n if (expr.startsWith('typeDefault.') && !/[()?]/.test(expr) && !expr.includes(':')) {\n const key = expr.slice('typeDefault.'.length);\n const v = ctx.typeDefault ? ctx.typeDefault[key] : undefined;\n return v == null ? EMPTY_SENTINEL : String(v);\n }\n if (expr.length > 80) return EMPTY_SENTINEL;\n if (!SAFE_EXPR.test(expr) || FORBIDDEN.test(expr)) return EMPTY_SENTINEL;\n const dotChain = expr.match(/\\./g);\n if (dotChain && dotChain.length > 1) return EMPTY_SENTINEL;\n try {\n const out = safeEval(expr, ctx);\n const str = out == null ? '' : String(out);\n if (REFLECTIVE_RE.test(str)) return EMPTY_SENTINEL;\n return str || EMPTY_SENTINEL;\n } catch {\n return EMPTY_SENTINEL;\n }\n}\n// #endregion\n\n// #region Cell Scrubbing\n/** Pattern matching reflective/introspective APIs that must be stripped from output. */\nconst REFLECTIVE_RE = /Reflect|Proxy|ownKeys/;\n\nfunction postProcess(s: string): string {\n if (!s) return s;\n return s.replace(new RegExp(EMPTY_SENTINEL, 'g'), '').replace(/Reflect\\.[^<>{}\\s]+|\\bProxy\\b|ownKeys\\([^)]*\\)/g, '');\n}\n\nexport function finalCellScrub(cell: HTMLElement): void {\n if (!REFLECTIVE_RE.test(cell.textContent || '')) return;\n // First pass: clear only text nodes containing forbidden tokens\n for (const n of cell.childNodes) {\n if (n.nodeType === Node.TEXT_NODE && REFLECTIVE_RE.test(n.textContent || '')) n.textContent = '';\n }\n // If forbidden tokens persist in element nodes, nuke everything\n if (REFLECTIVE_RE.test(cell.textContent || '')) {\n cell.textContent = '';\n }\n}\n// #endregion\n\n// #region Template Compilation\nexport function compileTemplate(raw: string) {\n if (REFLECTIVE_RE.test(raw)) {\n const fn = (() => '') as CompiledViewFunction;\n fn.__blocked = true;\n return fn;\n }\n const fn = ((ctx: EvalContext) => evalTemplateString(raw, ctx)) as CompiledViewFunction;\n fn.__blocked = false;\n return fn;\n}\n// #endregion\n","/**\n * Non-drag alternative for resizing — WCAG 2.2 SC 2.5.7 \"Dragging Movements\".\n *\n * Dragging a splitter is the primary gesture, but a single-pointer user\n * (trackball, head pointer, eye-gaze, speech mouse emulator) cannot reliably\n * press-hold-move-release. Tapping the splitter instead opens this popover,\n * which offers the same functionality through plain clicks: step buttons, a\n * numeric input, and a reset. A keyboard alternative alone would **not**\n * satisfy the criterion — it is specifically about pointer input.\n *\n * Target-agnostic on purpose: core column resizing and the shell's tool-panel\n * splitter share it, so every resizable thing in the grid presents the same\n * control instead of one-off menus.\n *\n * This module is **internal only** — NOT exported from `src/public.ts`.\n *\n * @since 3.6.0\n * @internal\n */\n\n/** Default size change per click of the − / + buttons, in px. */\nconst DEFAULT_STEP_PX = 16;\n\n/** Gap between the anchor handle and the popover, in px. */\nconst ANCHOR_GAP_PX = 6;\n\n/** Runtime check — happy-dom and older browsers may lack the Popover API. */\nfunction supportsPopover(): boolean {\n return typeof HTMLElement.prototype?.showPopover === 'function';\n}\n\n/**\n * How the popover reads and writes whatever it is sizing. `T` identifies the\n * target — a column index for the header, the panel element for the shell.\n *\n * Supplied by the owning controller so the drag and the click path share one\n * clamping/commit implementation.\n */\nexport interface SizePopoverOptions<T> {\n /** DOM id for the popover element, unique per owning controller. */\n id: string;\n /** Size change per click of the − / + buttons. Defaults to 16px. */\n step?: number;\n /** Current rendered size of the target, in px. */\n getSize(target: T): number;\n /** Commit an explicit size (implementation clamps to its own limits). */\n setSize(target: T, size: number): void;\n /** Restore the target's configured size. */\n reset(target: T): void;\n /** Accessible name for the popover, e.g. `\"Width of column Name\"`. */\n getLabel(target: T): string;\n /**\n * The grid host. The popover is mounted INSIDE it (the grid renders in light\n * DOM) so it inherits the theme's custom properties and `color-scheme`; the\n * top layer keeps it clear of ancestor overflow. `buildBareGridDOMIntoElement`\n * preserves foreign direct children, so it survives re-renders.\n */\n getHost(target: T): HTMLElement | null;\n}\n\nexport interface SizePopover<T> {\n /** Show the popover for `target`, positioned against `anchor`. */\n open(target: T, anchor: HTMLElement): void;\n /** Hide the popover. Safe to call when already closed. */\n close(): void;\n /** Remove the popover element and every document-level listener. */\n dispose(): void;\n /** The popover element, or `null` before the first {@link SizePopover.open}. */\n readonly element: HTMLElement | null;\n}\n\nfunction button(label: string, text: string): HTMLButtonElement {\n const el = document.createElement('button');\n el.type = 'button';\n el.className = 'tbw-size-popover-btn';\n el.setAttribute('aria-label', label);\n el.textContent = text;\n return el;\n}\n\nexport function createSizePopover<T>(options: SizePopoverOptions<T>): SizePopover<T> {\n const step = options.step ?? DEFAULT_STEP_PX;\n let root: HTMLDivElement | null = null;\n let input: HTMLInputElement | null = null;\n let anchorEl: HTMLElement | null = null;\n let target: T;\n let opened = false;\n let disposed = false;\n const abort = new AbortController();\n\n function close(): void {\n if (!opened || !root) return;\n opened = false;\n if (supportsPopover()) root.hidePopover();\n root.hidden = true;\n anchorEl?.removeAttribute('data-tbw-size-open');\n anchorEl = null;\n }\n\n function sync(): void {\n if (input) input.value = String(Math.round(options.getSize(target)));\n }\n\n function apply(size: number): void {\n if (!Number.isFinite(size)) return;\n options.setSize(target, size);\n sync();\n }\n\n function build(): HTMLDivElement {\n const el = document.createElement('div');\n el.id = options.id;\n el.className = 'tbw-size-popover';\n el.setAttribute('popover', 'manual');\n el.setAttribute('role', 'group');\n // Without the Popover API there is no UA `display: none`, so hide it here.\n el.hidden = true;\n\n const minus = button('Narrower', '\\u2212');\n const plus = button('Wider', '+');\n const reset = button('Reset size', 'Reset');\n\n const field = document.createElement('input');\n field.type = 'number';\n field.className = 'tbw-size-popover-input';\n field.min = '0';\n // `step` belongs to the − / + buttons, not the field: a measured size is\n // rarely a multiple of it, and a step mismatch would mark the field\n // `:invalid` and expose `aria-invalid=\"true\"` for a perfectly valid width.\n field.step = 'any';\n field.setAttribute('aria-label', 'Size in pixels');\n\n minus.addEventListener('click', () => apply(options.getSize(target) - step), { signal: abort.signal });\n plus.addEventListener('click', () => apply(options.getSize(target) + step), { signal: abort.signal });\n reset.addEventListener(\n 'click',\n () => {\n options.reset(target);\n sync();\n },\n { signal: abort.signal },\n );\n field.addEventListener('change', () => apply(field.valueAsNumber), { signal: abort.signal });\n\n // Tabbing past either end dismisses instead of stranding the user — the\n // popover has no visible trigger to tab back to. A null `relatedTarget`\n // (click focus in Firefox/Safari, focus leaving the document) is ignored.\n el.addEventListener(\n 'focusout',\n (e) => {\n const next = (e as FocusEvent).relatedTarget;\n if (next instanceof Node && !el.contains(next)) close();\n },\n { signal: abort.signal },\n );\n\n el.append(minus, field, plus, reset);\n input = field;\n return el;\n }\n\n /**\n * Position against the anchor in viewport coordinates. The popover lives in\n * the top layer, so its containing block is the viewport regardless of where\n * the anchor sits in the DOM.\n *\n * Called once per open and never again: repeatedly clicking − / + moves the\n * anchor, and following it would slide the button out from under the pointer\n * of the very users this control exists for. The anchor keeps its indicator\n * lit instead (`data-tbw-size-open`) to show which edge is being sized.\n */\n function place(anchor: HTMLElement): void {\n if (!root) return;\n const a = anchor.getBoundingClientRect();\n const box = root.getBoundingClientRect();\n const clamp = (v: number, max: number) => Math.max(4, Math.min(v, max - 4));\n const left = clamp(a.left + a.width / 2 - box.width / 2, window.innerWidth - box.width);\n\n // A full-height anchor (the tool-panel splitter) has no meaningful \"below\":\n // it would land past the bottom of the grid. Sit against its middle instead.\n const below = a.bottom + ANCHOR_GAP_PX;\n const above = a.top - ANCHOR_GAP_PX - box.height;\n const tall = a.height > box.height * 3;\n let top: number;\n if (!tall && below + box.height <= window.innerHeight) top = below;\n else if (!tall && above >= 4) top = above;\n else top = clamp(a.top + a.height / 2 - box.height / 2, window.innerHeight - box.height);\n\n root.style.left = `${Math.round(left)}px`;\n root.style.top = `${Math.round(top)}px`;\n }\n\n // Any pointer press outside the popover dismisses it. Capture phase so the\n // press that lands on a grid cell still reaches its own handler afterwards.\n document.addEventListener(\n 'pointerdown',\n (e) => {\n if (opened && root && !e.composedPath().includes(root)) close();\n },\n { capture: true, signal: abort.signal },\n );\n document.addEventListener(\n 'keydown',\n (e) => {\n if ((e as KeyboardEvent).key === 'Escape') close();\n },\n { capture: true, signal: abort.signal },\n );\n\n return {\n get element() {\n return root;\n },\n open(next, anchor) {\n if (disposed) return;\n target = next;\n anchorEl?.removeAttribute('data-tbw-size-open');\n anchorEl = anchor;\n anchor.setAttribute('data-tbw-size-open', '');\n if (!root) root = build();\n const host = options.getHost(next) ?? document.body;\n if (root.parentNode !== host) host.appendChild(root);\n root.setAttribute('aria-label', options.getLabel(next));\n sync();\n root.hidden = false;\n if (!opened && supportsPopover()) root.showPopover();\n opened = true;\n place(anchor);\n input?.focus();\n },\n close,\n dispose() {\n disposed = true;\n close();\n abort.abort();\n root?.remove();\n root = null;\n input = null;\n },\n };\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/**\n * CSS selector for focusable editor elements within a cell.\n * Used by EditingPlugin, UndoRedoPlugin, and keyboard navigation. Defined here\n * (a leaf module) rather than in `rows.ts` so `keyboard.ts` can consume it\n * without creating a rows ↔ keyboard import cycle.\n */\nexport const FOCUSABLE_EDITOR_SELECTOR =\n 'input,select,textarea,[contenteditable=\"true\"],[contenteditable=\"\"],[tabindex]:not([tabindex=\"-1\"])';\n\n// #region CSS Classes\n\n/**\n * CSS class names used in the grid's DOM.\n * Use these when adding/removing classes or querying elements.\n *\n * Classes are organized by:\n * - Core: Used by the grid core for structure and basic functionality\n * - Shared: Used by multiple features/plugins, styled by core CSS\n *\n * @category Plugin Development\n * @since 0.1.1\n */\nexport const GridClasses = {\n // ─── Core Structure ───────────────────────────────────────────────\n ROOT: 'tbw-grid-root',\n HEADER: 'header',\n HEADER_ROW: 'header-row',\n HEADER_CELL: 'header-cell',\n\n // Body structure\n ROWS_VIEWPORT: 'rows-viewport',\n ROWS_SPACER: 'rows-spacer',\n ROWS_CONTAINER: 'rows',\n\n // Row elements\n DATA_ROW: 'data-grid-row',\n GROUP_ROW: 'group-row',\n\n // Cell elements\n DATA_CELL: 'cell',\n\n // ─── Core States ──────────────────────────────────────────────────\n SELECTED: 'selected',\n FOCUSED: 'focused',\n EDITING: 'editing',\n EXPANDED: 'expanded',\n COLLAPSED: 'collapsed',\n DRAGGING: 'dragging',\n RESIZING: 'resizing',\n\n // Sorting (core feature)\n SORTABLE: 'sortable',\n SORTED_ASC: 'sorted-asc',\n SORTED_DESC: 'sorted-desc',\n\n // Visibility\n HIDDEN: 'hidden',\n\n // ─── Shared Classes (used by plugins, styled by core CSS) ────────\n // These are defined here because core CSS provides the base styling.\n // Plugins apply these classes; core CSS defines how they look.\n\n // Sticky positioning (PinnedColumnsPlugin applies, core CSS styles)\n STICKY_LEFT: 'sticky-left',\n STICKY_RIGHT: 'sticky-right',\n\n // Pinned rows (PinnedRowsPlugin applies, core CSS styles)\n PINNED_TOP: 'pinned-top',\n PINNED_BOTTOM: 'pinned-bottom',\n\n // Tree structure (TreePlugin applies, core CSS styles)\n TREE_TOGGLE: 'tree-toggle',\n TREE_INDENT: 'tree-indent',\n\n // Grouping (GroupingRowsPlugin applies, core CSS styles)\n GROUP_TOGGLE: 'group-toggle',\n GROUP_LABEL: 'group-label',\n GROUP_COUNT: 'group-count',\n\n // Selection (SelectionPlugin applies, core CSS styles)\n RANGE_SELECTION: 'range-selection',\n SELECTION_OVERLAY: 'selection-overlay',\n} as const;\n\n// #endregion\n\n// #region Data Attributes\n\n/**\n * Data attribute names used on grid elements.\n * Use these when getting/setting data attributes.\n *\n * @category Plugin Development\n * @since 0.1.1\n */\nexport const GridDataAttrs = {\n // ─── Core Attributes ──────────────────────────────────────────────\n FIELD: 'data-field',\n\n /**\n * Marks a `title` the grid itself put on a cell to reveal ellipsised text\n * (WCAG SC 1.4.12). Anything that sets its own `title` on a cell must clear\n * this, or the grid will later mistake that title for its own and replace it.\n *\n * @since 3.6.0\n */\n TRUNCATED: 'data-tbw-truncated',\n\n // ─── Shared Attributes (used by plugins) ──────────────────────────\n GROUP_KEY: 'data-group-key', // GroupingRowsPlugin\n} as const;\n\n// #endregion\n\n// #region Selectors\n\n/**\n * Common CSS selectors for querying grid elements.\n * Built from the class constants for consistency.\n *\n * @category Plugin Development\n * @since 0.1.1\n */\nexport const GridSelectors = {\n ROOT: `.${GridClasses.ROOT}`,\n HEADER: `.${GridClasses.HEADER}`,\n HEADER_ROW: `.${GridClasses.HEADER_ROW}`,\n HEADER_CELL: `.${GridClasses.HEADER_CELL}`,\n ROWS_VIEWPORT: `.${GridClasses.ROWS_VIEWPORT}`,\n ROWS_CONTAINER: `.${GridClasses.ROWS_CONTAINER}`,\n DATA_ROW: `.${GridClasses.DATA_ROW}`,\n DATA_CELL: `.${GridClasses.DATA_CELL}`,\n GROUP_ROW: `.${GridClasses.GROUP_ROW}`,\n\n // By data attribute\n CELL_BY_FIELD: (field: string) => `.${GridClasses.DATA_CELL}[${GridDataAttrs.FIELD}=\"${field}\"]`,\n\n // State selectors\n SELECTED_ROWS: `.${GridClasses.DATA_ROW}.${GridClasses.SELECTED}`,\n EDITING_CELL: `.${GridClasses.DATA_CELL}.${GridClasses.EDITING}`,\n} as const;\n\n// #endregion\n\n// #region CSS Custom Properties\n\n/**\n * CSS custom property names for theming.\n * Use these when programmatically setting styles.\n *\n * @category Plugin Development\n * @since 0.1.1\n */\nexport const GridCSSVars = {\n // Colors\n COLOR_BG: '--tbw-color-bg',\n COLOR_FG: '--tbw-color-fg',\n COLOR_FG_MUTED: '--tbw-color-fg-muted',\n COLOR_BORDER: '--tbw-color-border',\n COLOR_ACCENT: '--tbw-color-accent',\n COLOR_HEADER_BG: '--tbw-color-header-bg',\n COLOR_HEADER_FG: '--tbw-color-header-fg',\n COLOR_SELECTION: '--tbw-color-selection',\n COLOR_ROW_HOVER: '--tbw-color-row-hover',\n COLOR_ROW_ALT: '--tbw-color-row-alt',\n\n // Sizing\n ROW_HEIGHT: '--tbw-row-height',\n HEADER_HEIGHT: '--tbw-header-height',\n CELL_PADDING: '--tbw-cell-padding',\n\n // Typography\n FONT_FAMILY: '--tbw-font-family',\n FONT_SIZE: '--tbw-font-size',\n\n // Borders\n BORDER_RADIUS: '--tbw-border-radius',\n FOCUS_OUTLINE: '--tbw-focus-outline',\n} as const;\n\n// #endregion\n\n// Type helpers\n\n/**\n * Union of all CSS class names used in the grid's DOM.\n * Use `GridClasses` to access individual values by key.\n *\n * @category Plugin Development\n * @since 0.1.1\n */\nexport type GridClassName = (typeof GridClasses)[keyof typeof GridClasses];\n\n/**\n * Union of all `data-*` attribute names used on grid elements.\n * Use `GridDataAttrs` to access individual values by key.\n *\n * @category Plugin Development\n * @since 0.1.1\n */\nexport type GridDataAttr = (typeof GridDataAttrs)[keyof typeof GridDataAttrs];\n\n/**\n * Union of all CSS custom property names (`--tbw-*`) available for theming.\n * Use `GridCSSVars` to access individual values by key.\n *\n * @category Plugin Development\n * @since 0.1.1\n */\nexport type GridCSSVar = (typeof GridCSSVars)[keyof typeof GridCSSVars];\n","/**\n * 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 { setSanitizedHTML } 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 setSanitizedHTML(toggleBtn, 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 * Panel close (✕) button. `data-panel-close` is wired by\n * `setupShellEventListeners` to close the panel.\n */\nfunction buildPanelCloseButton(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\n/** Slim standalone header row holding just the close button. */\nfunction buildPanelCloseRow(): HTMLDivElement {\n const panelHeader = div('tbw-tool-panel-header', { part: 'tool-panel-header' });\n panelHeader.appendChild(buildPanelCloseButton(false));\n return panelHeader;\n}\n\n/** Accordion header button: icon + title + chevron. */\nfunction buildAccordionHeader(\n panel: ShellBodyOptions['panels'][number],\n options: ShellBodyOptions,\n isSinglePanel: boolean,\n): HTMLButtonElement {\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 if (panel.icon) {\n const iconSpan = createElement('span', { class: 'tbw-accordion-icon' });\n setSanitizedHTML(iconSpan, panel.icon);\n headerBtn.appendChild(iconSpan);\n }\n\n // Title (empty span kept as a flex spacer so the chevron stays right-aligned)\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 setSanitizedHTML(chevronSpan, options.expandIcon);\n }\n headerBtn.appendChild(chevronSpan);\n }\n return headerBtn;\n}\n\n/** One accordion section (header row + empty content slot). */\nfunction buildAccordionSection(\n panel: ShellBodyOptions['panels'][number],\n options: ShellBodyOptions,\n isSinglePanel: boolean,\n inlineClose: boolean,\n): HTMLDivElement {\n // The accordion header row (icon + title + chevron/toggle) is skipped only\n // when a single title-less panel is registered — there is nothing to show and\n // no sibling to toggle between. With multiple panels a title-less header still\n // renders (empty title span) so the chevron and toggle button stay available.\n const renderHeader = !!panel.title || !isSinglePanel;\n // Inline close shares the header row; only possible when a header exists.\n const canInlineClose = inlineClose && renderHeader;\n const sectionClasses = `tbw-accordion-section${panel.isExpanded ? ' expanded' : ''}${isSinglePanel ? ' single' : ''}${canInlineClose ? ' has-inline-close' : ''}`;\n const section = div(sectionClasses, { 'data-section': panel.id });\n\n if (renderHeader) {\n const headerBtn = buildAccordionHeader(panel, options, isSinglePanel);\n if (canInlineClose) {\n // Single-panel close button shares the header row with the panel title.\n const headerRow = div('tbw-accordion-header-row');\n headerRow.appendChild(headerBtn);\n headerRow.appendChild(buildPanelCloseButton(true));\n section.appendChild(headerRow);\n } else {\n section.appendChild(headerBtn);\n }\n } else if (inlineClose) {\n // Single title-less panel with the header suppressed: the inline close\n // button has no header row to share, so render it as a standalone row.\n section.appendChild(buildPanelCloseRow());\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 return section;\n}\n\n/** The `<aside>` tool panel: resize handle, optional close affordance, accordion. */\nfunction buildToolPanel(options: ShellBodyOptions, mode: 'overlay' | 'push' | 'dropdown'): HTMLElement {\n const isSinglePanel = options.panels.length === 1;\n const 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 — draggable, and tappable for the SC 2.5.7 size menu.\n panelEl.appendChild(\n div('tbw-tool-panel-resize', {\n 'data-resize-handle': '',\n 'data-handle-position': options.position === 'left' ? 'right' : 'left',\n role: 'button',\n tabindex: '-1',\n 'aria-label': 'Tool panel width — drag, or activate to set a width',\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).\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 inlineClose = options.showCloseButton === true && isSinglePanel;\n if (options.showCloseButton && !isSinglePanel) {\n panelEl.appendChild(buildPanelCloseRow());\n }\n\n const panelContent = div('tbw-tool-panel-content', { role: 'presentation' });\n const accordion = div('tbw-accordion');\n for (const panel of options.panels) {\n accordion.appendChild(buildAccordionSection(panel, options, isSinglePanel, inlineClose));\n }\n panelContent.appendChild(accordion);\n panelEl.appendChild(panelContent);\n return panelEl;\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\n // Grid content wrapper with cloned grid structure\n const gridContent = div('tbw-grid-content');\n gridContent.appendChild(cloneGridContent());\n\n const panelEl = options.panels.length > 0 ? buildToolPanel(options, mode) : null;\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 { startPointerDrag } from '../../core/internal/pointer-drag';\nimport { escapeHtml, sanitizeHTML, setSanitizedHTML } from '../../core/internal/sanitize';\nimport { createSizePopover, type SizePopover } from '../../core/internal/size-popover';\nimport type { IconValue } from '../../core/types';\nimport type { HeaderContentDefinition, ShellConfig, ToolbarContentDefinition, ToolPanelDefinition } from './types';\n\n/** Tool-panel width change per click of the size popover's − / + buttons, in px. */\nconst RESIZE_STEP_PX = 32;\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 * 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 // Skip the header row only for a single title-less panel. With multiple\n // panels a title-less header still renders (empty title span) so the\n // expand/collapse chevron and toggle button remain available.\n const renderHeader = !!panel.title || !isSinglePanel;\n const headerHtml = renderHeader\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\">${escapeHtml(panel.title ?? '')}</span>\n ${chevronHtml}\n </button>`\n : '';\n accordionHtml += `\n <div class=\"${sectionClasses}\" data-section=\"${panel.id}\">\n ${headerHtml}\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}\" role=\"button\" tabindex=\"-1\" aria-label=\"Tool panel width — drag, or activate to set a width\"></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` (optional): Panel title shown in accordion header. Omit it when the\n * panel content provides its own heading (see {@link ToolPanelDefinition.title}).\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') ?? undefined;\n\n // Skip only if the required `id` is missing. `title` is optional (#430):\n // a title-less panel renders its content without an accordion header row\n // (single panel) or with an empty title span (multiple panels).\n if (!id) {\n warnDiagnostic(TOOL_PANEL_MISSING_ATTR, `Tool panel missing required id attribute: title=\"${title ?? ''}\"`);\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 light-DOM markup 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 rawContent = panelEl.innerHTML.trim();\n render = (container: HTMLElement) => {\n const wrapper = document.createElement('div');\n setSanitizedHTML(wrapper, rawContent);\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.title !== title ||\n existingPanel.order !== order ||\n existingPanel.icon !== icon ||\n 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.title = title;\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 *\n * The handle is draggable **and** tappable: pressing and releasing without\n * moving opens a click-only size menu (WCAG 2.2 SC 2.5.7).\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 let moved = false;\n let cancelDrag: (() => void) | null = null;\n let sizePopover: SizePopover<HTMLElement> | null = null;\n\n const availableMax = () => shellBody.getBoundingClientRect().width - 20;\n\n const openSizePopover = () => {\n sizePopover ??= createSizePopover<HTMLElement>({\n id: 'tbw-tool-panel-width-popover',\n step: RESIZE_STEP_PX,\n getSize: (el) => el.getBoundingClientRect().width,\n // Same clamp as `onMove`, so no pointer path can reach a size the drag cannot.\n setSize: (el, width) => {\n const clamped = Math.min(availableMax(), Math.max(minWidth, width));\n el.style.width = `${clamped}px`;\n onResize(clamped);\n },\n reset: (el) => {\n el.style.width = '';\n onResize(el.getBoundingClientRect().width);\n },\n getLabel: () => 'Tool panel width',\n getHost: (el) => el.closest<HTMLElement>('tbw-grid, [data-tbw-grid]'),\n });\n sizePopover.open(panel, handle);\n };\n\n const onMove = (e: PointerEvent) => {\n if (!isResizing) return;\n e.preventDefault();\n moved = true;\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 /**\n * Restore chrome after a drag. `commit` is false when the drag was aborted\n * (pointercancel / Escape), in which case the caller has already restored the\n * original width and no `onResize` notification should fire.\n */\n const finish = (commit: boolean) => {\n if (!isResizing) return;\n isResizing = false;\n cancelDrag = null;\n handle.classList.remove('resizing');\n panel.style.transition = ''; // Re-enable transition\n document.body.style.cursor = '';\n document.body.style.userSelect = '';\n\n if (commit) {\n // Get final width and notify\n const finalWidth = panel.getBoundingClientRect().width;\n onResize(finalWidth);\n }\n };\n\n const onPointerDown = (e: PointerEvent) => {\n if (isResizing) return;\n // Only the primary mouse button starts a splitter drag — a right-click must\n // keep its context menu rather than being preventDefault()-ed away.\n if (e.button !== 0 && e.pointerType === 'mouse') return;\n e.preventDefault();\n isResizing = true;\n moved = false;\n startX = e.clientX;\n startWidth = panel.getBoundingClientRect().width;\n // Calculate max width dynamically based on grid container width\n maxWidth = availableMax(); // 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 cancelDrag = startPointerDrag(e, handle, {\n onMove,\n onEnd: () => {\n // `startPointerDrag` promotes immediately, so tap-vs-drag can only be\n // told apart by whether `onMove` ever ran.\n const wasTap = !moved;\n finish(!wasTap);\n if (wasTap) openSizePopover();\n },\n onCancel: () => {\n // Aborted (pointercancel / Escape) — snap back to the pre-drag width.\n panel.style.width = `${startWidth}px`;\n finish(false);\n },\n });\n };\n\n handle.addEventListener('pointerdown', onPointerDown);\n\n // Return cleanup function\n return () => {\n handle.removeEventListener('pointerdown', onPointerDown);\n cancelDrag?.();\n cancelDrag = null;\n sizePopover?.dispose();\n sizePopover = null;\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 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 Styles\n *\n * Merged from the former core partials `core/styles/shell.css` +\n * `core/styles/tool-panel.css` (extraction #370). Near-verbatim concatenation —\n * cascade layer (`@layer tbw-base`) preserved so behavior is byte-identical\n * while both copies coexist during Phase 1a. Core copies are removed in\n * Task 1b.1.5; the layer question (tbw-base vs tbw-plugins) is revisited then.\n *\n * EXCEPTION: `.tbw-scroll-area` and `.tbw-grid-content` are NOT here — both are\n * core scroll-viewport layout (rendered by dom-builder for every grid, shell or\n * not) and live in `core/styles/base.css`. Without a shell, `.tbw-grid-content`\n * still wraps the scroll area, so its layout rule must load unconditionally.\n *\n * @layer tbw-base\n */\n\n/* ==========================================================================\n * Shell Layout (was core/styles/shell.css)\n *\n * Styles for the grid shell including header, toolbar, content area,\n * and scroll container.\n * ========================================================================== */\n\n@layer tbw-base {\n tbw-grid {\n .tbw-shell-header {\n display: flex;\n align-items: center;\n gap: 8px;\n min-height: var(--tbw-shell-header-height);\n padding: 0 8px;\n background: var(--tbw-shell-header-bg);\n border-bottom: 1px solid var(--tbw-shell-header-border);\n flex-shrink: 0;\n }\n\n .tbw-shell-title {\n font-size: var(--tbw-shell-title-font-size);\n font-weight: var(--tbw-shell-title-font-weight);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n }\n\n .tbw-shell-content {\n flex: 1;\n display: flex;\n align-items: center;\n gap: 12px;\n min-width: 0;\n overflow: hidden;\n }\n\n .tbw-shell-toolbar {\n display: flex;\n align-items: center;\n gap: var(--tbw-toolbar-button-gap);\n flex-shrink: 0;\n }\n\n .tbw-toolbar-btn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: var(--tbw-toolbar-button-size);\n height: var(--tbw-toolbar-button-size);\n padding: 0;\n border: 1px solid transparent;\n border-radius: var(--tbw-border-radius);\n background: transparent;\n color: var(--tbw-color-fg);\n cursor: pointer;\n font-size: 16px;\n transition:\n background var(--tbw-transition-duration) var(--tbw-transition-ease),\n border-color var(--tbw-transition-duration) var(--tbw-transition-ease);\n\n &:hover {\n background: var(--tbw-color-row-hover);\n }\n\n &:focus-visible {\n outline: var(--tbw-focus-outline);\n outline-offset: var(--tbw-focus-outline-offset);\n }\n\n &.active {\n background: var(--tbw-focus-background);\n border-color: var(--tbw-color-accent);\n }\n\n &:disabled {\n opacity: 0.5;\n cursor: not-allowed;\n }\n }\n\n .tbw-toolbar-separator {\n width: 1px;\n height: 20px;\n background: var(--tbw-color-border);\n margin: 0 4px;\n }\n\n .tbw-shell-body {\n position: relative;\n display: flex;\n flex: 1;\n min-height: 0;\n overflow: visible;\n }\n }\n}\n\n/* ==========================================================================\n * Tool Panel (was core/styles/tool-panel.css)\n *\n * Styles for the collapsible side panel including header, content,\n * resize handle, and accordion components.\n * ========================================================================== */\n\n@layer tbw-base {\n tbw-grid {\n .tbw-tool-panel {\n position: absolute;\n top: 0;\n bottom: 0;\n right: 0;\n width: 0;\n /* `clip` rather than `hidden` so the clip region can be widened: the\n splitter's hit area straddles the docked edge and has to survive it.\n `overflow-clip-margin` is the one place `calc()` is avoided — Chromium\n drops the declaration when the value is calculated, so the overhang is\n its own token and the splitter geometry is derived from it. */\n overflow: clip;\n overflow-clip-margin: var(--tbw-tool-panel-resize-overhang, 12px);\n background: var(--tbw-tool-panel-bg);\n border-left: 1px solid var(--tbw-tool-panel-border);\n transition: width var(--tbw-tool-panel-transition);\n display: flex;\n flex-direction: column;\n z-index: var(--tbw-z-layer-toolpanel, 31);\n box-shadow: -2px 0 8px var(--tbw-color-shadow);\n\n &[data-position='left'] {\n right: auto;\n left: 0;\n border-left: none;\n border-right: 1px solid var(--tbw-tool-panel-border);\n box-shadow: 2px 0 8px var(--tbw-color-shadow);\n }\n\n &.open {\n /* Never wider than the grid it docks into. An overflowing panel is cut\n off by the clip above with no way to scroll to what is missing — the\n labels and the left half of every chip simply vanish (SC 1.4.10\n Reflow). The 20px reserve matches the drag clamp in\n `setupToolPanelResize`, so no path can reach a size the other cannot.\n `max-width` as well as `width`: a dragged width lands as an inline\n style, which only a max-width can rein back in when the grid later\n narrows under it. */\n width: min(var(--tbw-tool-panel-width), 100% - 20px);\n max-width: calc(100% - 20px);\n }\n }\n\n /* ========== Push mode ========== */\n\n /* In push mode the panel becomes a flex sibling of the grid content\n instead of overlaying it. Opening the panel shrinks the grid's\n available width; the grid's ResizeObserver re-runs column\n virtualization, so cells that would be hidden by an overlay panel\n remain reachable.\n\n The overlay shadow is dropped (nothing to lift off) and the resize\n handle is suppressed: the panel's width is part of the layout, so\n drag-to-resize would fight the flex container instead of cooperating\n with it. */\n .tbw-shell-body[data-mode='push'] {\n .tbw-tool-panel {\n position: relative;\n top: auto;\n bottom: auto;\n right: auto;\n left: auto;\n flex-shrink: 0;\n box-shadow: none;\n z-index: auto;\n /* In overlay mode the 1px side border is harmless because the panel\n floats above the grid. In push mode the panel is a flex sibling,\n so a non-zero border on a width:0 panel still reserves 1px of\n horizontal space and shows as a stray divider. Drop the border\n by default and only re-apply it on the open state. */\n border-left: none;\n border-right: none;\n\n &.open {\n border-left: 1px solid var(--tbw-tool-panel-border);\n }\n\n &[data-position='left'].open {\n border-left: none;\n border-right: 1px solid var(--tbw-tool-panel-border);\n }\n }\n\n .tbw-tool-panel-resize {\n display: none;\n }\n }\n\n /* ========== Dropdown mode ========== */\n\n /* In dropdown mode the whole tool-panel sidebar (the full accordion) is\n promoted to the top layer as a native popover and anchored near a\n trigger (the built-in toggle, a custom column-header button, or the grid\n corner). The docked absolute/full-height sidebar styling is replaced by\n a self-sized floating card with an internal scroll region.\n\n The resize handle is suppressed (a popover has no docked edge to drag)\n and the open/close width transition is dropped (the popover is shown and\n hidden via the Popover API, not by animating width). */\n .tbw-tool-panel[popover] {\n position: fixed;\n inset: auto;\n width: var(--tbw-tool-panel-width);\n max-width: calc(100vw - 16px);\n height: auto;\n max-height: min(70vh, 480px);\n overflow: hidden auto;\n margin: 4px 0 0 0;\n padding: 0;\n border: 1px solid var(--tbw-tool-panel-border);\n border-radius: var(--tbw-border-radius, 6px);\n box-shadow: 0 8px 24px var(--tbw-color-shadow);\n transition: none;\n\n /* The native popover UA rule hides a closed popover with `display: none`,\n but the base `.tbw-tool-panel { display: flex }` author rule outranks\n the UA origin and would leak the closed popover onto the page (floating\n over the header). Re-assert hidden-until-open here, and restore the\n flex layout only while the popover is actually shown. */\n display: none;\n\n /* `:popover-open` is the native (Popover API) open signal. `.open` is the\n JS-toggled fallback (`updatePanelState`) so dropdown mode still renders\n in browsers / DOMs without Popover API support, where `showPopover()`\n is unavailable and `:popover-open` never matches. */\n &:popover-open,\n &.open {\n display: flex;\n width: var(--tbw-tool-panel-width);\n }\n\n .tbw-tool-panel-resize {\n display: none;\n }\n }\n\n /* CSS anchor positioning (progressive enhancement). When the browser\n supports it, the controller wires `anchor-name` on the trigger and\n `position-anchor` on the popover; JS only sets fixed coordinates as a\n fallback for browsers without anchor positioning. */\n @supports (anchor-name: --x) {\n .tbw-tool-panel[popover] {\n /* Default (right position): align popover's right edge to the anchor's\n right edge and drop below it. */\n top: anchor(bottom);\n right: anchor(right);\n left: auto;\n position-try-fallbacks:\n flip-block,\n flip-inline,\n flip-block flip-inline;\n\n /* Grid-corner fallback anchors to the top of the grid host. */\n &[data-anchor='corner'] {\n top: anchor(top);\n }\n\n &[data-position='left'] {\n left: anchor(left);\n right: auto;\n }\n }\n }\n\n .tbw-tool-panel-resize {\n position: absolute;\n top: 0;\n bottom: 0;\n /* Transparent hit area, twice the overhang wide (24px by default, the\n SC 2.5.8 minimum) and centred on the docked edge, so half of it falls\n outside the panel and it never covers the panel's own controls. */\n width: calc(var(--tbw-tool-panel-resize-overhang, 12px) * 2);\n cursor: col-resize;\n background: transparent;\n z-index: 10;\n /* The splitter drag is driven by pointer capture, so the browser must not\n claim the gesture for panning on touch devices. */\n touch-action: none;\n user-select: none;\n\n /* The visible bar is only the 6px at the docked edge; the rest of the\n width is invisible hit area. */\n &::before {\n content: '';\n position: absolute;\n top: 0;\n bottom: 0;\n width: 6px;\n background: transparent;\n transition: background var(--tbw-transition-duration) var(--tbw-transition-ease);\n }\n\n &[data-handle-position='left'] {\n left: calc(3px - var(--tbw-tool-panel-resize-overhang, 12px));\n\n &::before {\n left: calc(var(--tbw-tool-panel-resize-overhang, 12px) - 3px);\n }\n }\n\n &[data-handle-position='right'] {\n right: calc(3px - var(--tbw-tool-panel-resize-overhang, 12px));\n\n &::before {\n right: calc(var(--tbw-tool-panel-resize-overhang, 12px) - 3px);\n }\n }\n\n /* `[data-tbw-size-open]` keeps the splitter lit while the size popover is\n open, so the popover stays visually tied to the edge it is sizing. */\n &:hover::before,\n &.resizing::before,\n &[data-tbw-size-open]::before {\n background: var(--tbw-color-accent);\n }\n }\n\n /* A closed panel is zero-width, so the splitter would hang over the grid\n inside the clip margin. It has nothing to size until the panel is open. */\n .tbw-tool-panel:not(.open) .tbw-tool-panel-resize {\n display: none;\n }\n\n .tbw-tool-panel-header {\n display: flex;\n align-items: center;\n justify-content: flex-end;\n min-height: var(--tbw-tool-panel-header-height);\n padding: 0 12px;\n border-bottom: 1px solid var(--tbw-tool-panel-border);\n flex-shrink: 0;\n }\n\n .tbw-tool-panel-title {\n font-weight: 600;\n font-size: 13px;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n }\n\n .tbw-tool-panel-close {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 24px;\n height: 24px;\n padding: 0;\n border: none;\n border-radius: var(--tbw-border-radius);\n background: transparent;\n color: var(--tbw-color-fg-muted);\n cursor: pointer;\n font-size: 14px;\n\n &:hover {\n background: var(--tbw-color-row-hover);\n color: var(--tbw-color-fg);\n }\n }\n\n /* Inline close button — shares the first accordion header row in\n single-panel mode (header bar hidden, overlay/push mode). */\n .tbw-accordion-header-row {\n display: flex;\n align-items: stretch;\n\n .tbw-accordion-header {\n flex: 1;\n min-width: 0;\n }\n }\n\n .tbw-tool-panel-close--inline {\n align-self: center;\n flex-shrink: 0;\n margin-inline-end: 8px;\n }\n\n .tbw-tool-panel-content {\n flex: 1;\n overflow: auto;\n }\n\n /* ========== Accordion ========== */\n\n .tbw-accordion {\n display: flex;\n flex-direction: column;\n gap: 0;\n }\n\n .tbw-accordion-section {\n border-bottom: 1px solid var(--tbw-tool-panel-border);\n\n &:last-child {\n border-bottom: none;\n }\n\n &.single .tbw-accordion-header {\n cursor: default;\n\n &:hover {\n background: transparent;\n }\n }\n\n &.expanded {\n .tbw-accordion-chevron {\n transform: rotate(90deg);\n }\n\n .tbw-accordion-content {\n display: block;\n }\n }\n }\n\n .tbw-accordion-header {\n display: flex;\n align-items: center;\n gap: 8px;\n width: 100%;\n padding: 10px 12px;\n border: none;\n background: transparent;\n color: var(--tbw-color-fg);\n font-size: 13px;\n font-weight: 600;\n text-align: start;\n cursor: pointer;\n user-select: none;\n\n &:hover {\n background: var(--tbw-color-row-hover);\n }\n }\n\n .tbw-accordion-chevron {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 16px;\n height: 16px;\n font-size: 10px;\n color: var(--tbw-color-fg-muted);\n transition: transform 0.15s ease;\n flex-shrink: 0;\n }\n\n .tbw-accordion-icon {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 16px;\n height: 16px;\n font-size: 14px;\n flex-shrink: 0;\n }\n\n .tbw-accordion-title {\n flex: 1;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n }\n\n .tbw-accordion-content {\n display: none;\n }\n }\n}\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// `Symbol.for` so every bundled copy of this module shares the key.\nconst CONFIGURED_PANEL_WIDTH: unique symbol = Symbol.for('@toolbox-web/grid:shell-configured-panel-width');\n/** Host carrying the last configured `toolPanel.width` written by any ShellPlugin instance (#575). */\ntype PanelWidthHost = HTMLElement & { [CONFIGURED_PANEL_WIDTH]?: string };\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')`. The equivalent `<tbw-grid>` delegate\n // methods were removed in v3 (extraction #370). Each guards on `hasState`\n // so a call 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 // Applied on merge, not render: a width-only config change may not render.\n if (grid) this.#syncConfiguredPanelWidth(grid, config.shell.toolPanel?.width);\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 /**\n * @internal Bridge `toolPanel.width` to the `--tbw-tool-panel-width` host\n * variable the shell CSS sizes the panel from (#575). Writes only when the\n * configured value changes; on removal it clears the variable only if it\n * still holds the configured value (a dragged width is kept). The last\n * configured value is recorded on the host, not the instance, so a replaced\n * plugin instance still releases its predecessor's override.\n */\n #syncConfiguredPanelWidth(grid: GridElement, width: number | undefined): void {\n const host: PanelWidthHost = grid._hostElement;\n const next = typeof width === 'number' && Number.isFinite(width) && width > 0 ? `${width}px` : undefined;\n const previous = host[CONFIGURED_PANEL_WIDTH];\n if (next === previous) return;\n host[CONFIGURED_PANEL_WIDTH] = next;\n const style = host.style;\n if (next) {\n style.setProperty('--tbw-tool-panel-width', next);\n // A drag leaves an inline width on the open panel that outranks the variable.\n grid._renderRoot.querySelector<HTMLElement>('.tbw-tool-panel')?.style.removeProperty('width');\n } else if (style.getPropertyValue('--tbw-tool-panel-width') === previous) {\n style.removeProperty('--tbw-tool-panel-width');\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 // `initialState` (or `locked`) is authoritative when set. Otherwise a\n // `defaultOpen` section implies the sidebar opens on load. See issue #259\n // for the proposal to split these two concerns.\n // Search marker: TOOLPANEL-OPEN-259\n const shouldOpenOnLoad =\n toolPanelCfg?.locked === true ||\n toolPanelCfg?.initialState === 'open' ||\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"],"mappings":"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"}