From 0a9cf42980a7f95627ce3cf0a5a9a18b128624a4 Mon Sep 17 00:00:00 2001 From: Mathias Elle Date: Wed, 27 May 2026 15:46:18 +0200 Subject: [PATCH 01/13] feat: enhance InspectorHints to inject data attributes into root HTML elements --- .../Decorator/InspectorHints.php | 43 +++-- src/view/frontend/web/js/inspector/dom.js | 148 +++++------------- 2 files changed, 70 insertions(+), 121 deletions(-) diff --git a/src/Model/TemplateEngine/Decorator/InspectorHints.php b/src/Model/TemplateEngine/Decorator/InspectorHints.php index 138768c..ba22283 100644 --- a/src/Model/TemplateEngine/Decorator/InspectorHints.php +++ b/src/Model/TemplateEngine/Decorator/InspectorHints.php @@ -177,7 +177,13 @@ public function render(BlockInterface $block, $templateFile, array $dictionary = } /** - * Inject MageForge inspector comment markers into HTML + * Inject MageForge inspector data attributes into the first root HTML element + * + * Injects data-mageforge-id and data-mageforge-block on the opening tag of the + * first HTML element in the output. If the content does not start with an HTML + * element (e.g. a plain URL or text fragment used inside an href attribute by a + * parent PageBuilder template), injection is skipped entirely to avoid corrupting + * the surrounding markup. * * @param string $html * @param BlockInterface $block @@ -227,25 +233,40 @@ private function injectInspectorAttributes( 'cache' => $formattedMetrics['cache'], ]; - // JSON encode with proper escaping for HTML comments $jsonMetadata = json_encode($metadata, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); if ($jsonMetadata === false) { return $html; } - // Escape any comment terminators in JSON to prevent breaking out of comment - $jsonMetadata = str_replace('-->', '-->', $jsonMetadata); - - // Wrap content with comment markers - $wrappedHtml = sprintf( - "\n%s\n", - $jsonMetadata, + // Escape single quotes so JSON can be safely embedded in a single-quoted HTML attribute. + // The browser automatically decodes HTML entities when getAttribute() is called, + // so JSON.parse() on the JS side will receive the correct string. + $safeJson = str_replace("'", ''', $jsonMetadata); + + // Inject data-mageforge-* attributes on the first root HTML element. + // This avoids HTML comment nodes which corrupt markup when block output is + // embedded inside HTML attribute values (e.g. PageBuilder URL blocks in href="..."). + $replaced = false; + $result = preg_replace_callback( + '/^(\s*<[a-zA-Z][a-zA-Z0-9]*)/s', + function (array $matches) use ($wrapperId, $safeJson, &$replaced): string { + $replaced = true; + return $matches[0] + . ' data-mageforge-id="' . $wrapperId . '"' + . ' data-mageforge-block=\'' . $safeJson . "'"; + }, $html, - $wrapperId, + 1, ); - return $wrappedHtml; + // If content doesn't start with an HTML element (e.g. plain text, URLs), + // skip injection to avoid corrupting attribute values in parent templates. + if (!$replaced || $result === null) { + return $html; + } + + return $result; } /** diff --git a/src/view/frontend/web/js/inspector/dom.js b/src/view/frontend/web/js/inspector/dom.js index 3f55303..3a07329 100644 --- a/src/view/frontend/web/js/inspector/dom.js +++ b/src/view/frontend/web/js/inspector/dom.js @@ -4,78 +4,20 @@ export const domMethods = { /** - * Parse MageForge comment markers in DOM - */ - parseCommentMarker(comment) { - const text = comment.textContent.trim(); - - // Check if it's a start marker - if (text.startsWith('MAGEFORGE_START ')) { - const jsonStr = text.substring('MAGEFORGE_START '.length); - try { - // Unescape any escaped comment terminators - const unescapedJson = jsonStr.replace(/-->/g, '-->'); - return { - type: 'start', - data: JSON.parse(unescapedJson) - }; - } catch (e) { - console.error('Failed to parse MageForge start marker:', e); - return null; - } - } - - // Check if it's an end marker - if (text.startsWith('MAGEFORGE_END ')) { - const id = text.substring('MAGEFORGE_END '.length).trim(); - return { - type: 'end', - id: id - }; - } - - return null; - }, - - /** - * Find all MageForge block regions in DOM + * Find all MageForge block elements in DOM. + * + * Blocks are identified by the data-mageforge-id attribute injected by + * InspectorHints on the first root HTML element of each rendered block. + * + * @returns {Array<{ data: Object, elements: Element[] }>} */ findAllMageForgeBlocks() { const blocks = []; - const walker = document.createTreeWalker( - document.body, - NodeFilter.SHOW_COMMENT, - null - ); - - const stack = []; - let comment; - - while ((comment = walker.nextNode())) { - const parsed = this.parseCommentMarker(comment); - - if (!parsed) continue; - - if (parsed.type === 'start') { - stack.push({ - startComment: comment, - data: parsed.data, - elements: [] - }); - } else if (parsed.type === 'end' && stack.length > 0) { - const currentBlock = stack[stack.length - 1]; - if (currentBlock.data.id === parsed.id) { - currentBlock.endComment = comment; - - // Collect all elements between start and end comments - currentBlock.elements = this.getElementsBetweenComments( - currentBlock.startComment, - currentBlock.endComment - ); - - blocks.push(currentBlock); - stack.pop(); - } + const elements = document.querySelectorAll('[data-mageforge-id]'); + for (const el of elements) { + const block = this._parseBlockElement(el); + if (block) { + blocks.push(block); } } @@ -83,55 +25,41 @@ export const domMethods = { }, /** - * Get all elements between two comment nodes + * Parse block metadata from an element's data-mageforge-block attribute. + * + * @param {Element} el + * @returns {{ data: Object, elements: Element[] }|null} */ - getElementsBetweenComments(startComment, endComment) { - const elements = []; - let node = startComment.nextSibling; + _parseBlockElement(el) { + const blockJson = el.getAttribute('data-mageforge-block'); + if (!blockJson) return null; - while (node && node !== endComment) { - if (node.nodeType === Node.ELEMENT_NODE) { - elements.push(node); - // Also add all descendants - elements.push(...node.querySelectorAll('*')); - } - node = node.nextSibling; + try { + const data = JSON.parse(blockJson); + data.id = el.getAttribute('data-mageforge-id'); + return { + data, + elements: [el, ...el.querySelectorAll('*')], + }; + } catch (e) { + console.error('Failed to parse MageForge block data:', e); + return null; } - - return elements; }, /** - * Find MageForge block data for a given element + * Find the MageForge block that contains a given element. + * + * Walks up the DOM via closest() to find the nearest ancestor (or self) + * that carries a data-mageforge-id attribute. + * + * @param {Element} element + * @returns {{ data: Object, elements: Element[] }|null} */ findBlockForElement(element) { - // Cache blocks for performance - if (!this.cachedBlocks || Date.now() - this.lastBlocksCacheTime > 1000) { - this.cachedBlocks = this.findAllMageForgeBlocks(); - this.lastBlocksCacheTime = Date.now(); - } - - let closestBlock = null; - let closestDepth = -1; - - // Find the deepest (most specific) block containing this element - for (const block of this.cachedBlocks) { - if (block.elements.includes(element)) { - // Calculate depth (how many ancestors between element and body) - let depth = 0; - let node = element; - while (node && node !== document.body) { - depth++; - node = node.parentElement; - } - - if (depth > closestDepth) { - closestBlock = block; - closestDepth = depth; - } - } - } + const blockEl = element.closest('[data-mageforge-id]'); + if (!blockEl) return null; - return closestBlock; + return this._parseBlockElement(blockEl); }, }; From dcce46d48b9b9deedb02ee82014719da70468561 Mon Sep 17 00:00:00 2001 From: Mathias Elle Date: Wed, 27 May 2026 15:56:11 +0200 Subject: [PATCH 02/13] feat: add CMS block identifier support in InspectorHints and tabs --- .../Decorator/InspectorHints.php | 4 +++ src/view/frontend/web/js/inspector/tabs.js | 26 ++++++++++++------- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/src/Model/TemplateEngine/Decorator/InspectorHints.php b/src/Model/TemplateEngine/Decorator/InspectorHints.php index ba22283..9d43399 100644 --- a/src/Model/TemplateEngine/Decorator/InspectorHints.php +++ b/src/Model/TemplateEngine/Decorator/InspectorHints.php @@ -219,6 +219,9 @@ private function injectInspectorAttributes( $cacheMetrics = $this->cacheCollector->getCacheInfo($block); $formattedMetrics = $this->cacheCollector->formatMetricsForJson($renderMetrics, $cacheMetrics); + // Detect CMS block identifier (e.g. for PageBuilder blocks rendered via Magento\Cms\Block\Block) + $cmsBlockId = method_exists($block, 'getBlockId') ? (string) $block->getBlockId() : ''; + // Build metadata as JSON $metadata = [ 'id' => $wrapperId, @@ -229,6 +232,7 @@ private function injectInspectorAttributes( 'parent' => $parentBlock, 'alias' => $blockAlias, 'override' => $isOverride, + 'cmsBlockId' => $cmsBlockId, 'performance' => $formattedMetrics['performance'], 'cache' => $formattedMetrics['cache'], ]; diff --git a/src/view/frontend/web/js/inspector/tabs.js b/src/view/frontend/web/js/inspector/tabs.js index 204b744..39e9dec 100644 --- a/src/view/frontend/web/js/inspector/tabs.js +++ b/src/view/frontend/web/js/inspector/tabs.js @@ -127,17 +127,18 @@ export const tabsMethods = { const src = parentBlock.data || {}; const parentData = { - template: src.template || '', - block: src.block || '', - module: src.module || '', - viewModel: src.viewModel || '', - parent: src.parent || '', - alias: src.alias || '', - override: src.override || '0', - blockClass: src.block || '', - parentBlock: src.parent || '', - blockAlias: src.alias || '', + template: src.template || '', + block: src.block || '', + module: src.module || '', + viewModel: src.viewModel || '', + parent: src.parent || '', + alias: src.alias || '', + override: src.override || '0', + blockClass: src.block || '', + parentBlock: src.parent || '', + blockAlias: src.alias || '', isOverride: src.override === '1', + cmsBlockId: src.cmsBlockId || '', }; // Inheritance note @@ -191,6 +192,11 @@ export const tabsMethods = { container.appendChild(this.createInfoSection('ViewModel', data.viewModel, '#22d3ee')); } + // CMS Block identifier (only for Magento_Cms blocks with PageBuilder content) + if (data.cmsBlockId) { + container.appendChild(this.createInfoSection('CMS Block', data.cmsBlockId, '#f472b6')); + } + // Module section container.appendChild(this.createInfoSection('Module', data.module, '#fbbf24')); }, From 0121ec8174e1e98d09e85832216b91ed1ef499b6 Mon Sep 17 00:00:00 2001 From: Mathias Elle Date: Wed, 27 May 2026 16:47:33 +0200 Subject: [PATCH 03/13] feat: enhance block detection for PageBuilder elements in inspector --- src/view/frontend/web/js/inspector/dom.js | 55 ++++++++++++++++++-- src/view/frontend/web/js/inspector/picker.js | 6 +++ 2 files changed, 57 insertions(+), 4 deletions(-) diff --git a/src/view/frontend/web/js/inspector/dom.js b/src/view/frontend/web/js/inspector/dom.js index 3a07329..4e64a7a 100644 --- a/src/view/frontend/web/js/inspector/dom.js +++ b/src/view/frontend/web/js/inspector/dom.js @@ -50,16 +50,63 @@ export const domMethods = { /** * Find the MageForge block that contains a given element. * - * Walks up the DOM via closest() to find the nearest ancestor (or self) - * that carries a data-mageforge-id attribute. + * Primary: walks up via closest() for the nearest [data-mageforge-id] ancestor. + * Fallback: for PageBuilder content with multiple root elements (rows), only the + * first root gets data-mageforge-id injected. Walk up to the root [data-content-type] + * element and search siblings for the nearest [data-mageforge-id]. * * @param {Element} element * @returns {{ data: Object, elements: Element[] }|null} */ findBlockForElement(element) { const blockEl = element.closest('[data-mageforge-id]'); - if (!blockEl) return null; + if (blockEl) return this._parseBlockElement(blockEl); - return this._parseBlockElement(blockEl); + // PageBuilder fallback: multi-root CMS blocks (e.g. multiple rows) + const rootPb = this._findRootPageBuilderElement(element); + if (rootPb) { + const sibling = this._findNearestMageForgeBlock(rootPb); + if (sibling) return this._parseBlockElement(sibling); + } + + return null; + }, + + /** + * Walk up the DOM to find the topmost [data-content-type] element (PageBuilder root). + * + * @param {Element} element + * @returns {Element|null} + */ + _findRootPageBuilderElement(element) { + let current = element; + let rootPb = null; + while (current && current !== document.body) { + if (current.hasAttribute('data-content-type')) { + rootPb = current; + } + current = current.parentElement; + } + return rootPb; + }, + + /** + * Search preceding and following siblings for the nearest [data-mageforge-id] element. + * + * @param {Element} element + * @returns {Element|null} + */ + _findNearestMageForgeBlock(element) { + let sibling = element.previousElementSibling; + while (sibling) { + if (sibling.hasAttribute('data-mageforge-id')) return sibling; + sibling = sibling.previousElementSibling; + } + sibling = element.nextElementSibling; + while (sibling) { + if (sibling.hasAttribute('data-mageforge-id')) return sibling; + sibling = sibling.nextElementSibling; + } + return null; }, }; diff --git a/src/view/frontend/web/js/inspector/picker.js b/src/view/frontend/web/js/inspector/picker.js index 302534e..ea7be3c 100644 --- a/src/view/frontend/web/js/inspector/picker.js +++ b/src/view/frontend/web/js/inspector/picker.js @@ -232,6 +232,12 @@ export const pickerMethods = { return target; } + // For PageBuilder elements where no block could be resolved (e.g. injection + // was skipped entirely), still open the inspector showing inherited/no-data state. + if (target.closest('[data-content-type]')) { + return target; + } + return null; }, }; From f47d4976315c0be2e9b5a23b08da4ad1fc679b66 Mon Sep 17 00:00:00 2001 From: Mathias Elle Date: Wed, 27 May 2026 17:14:35 +0200 Subject: [PATCH 04/13] feat: add magewire support for Inspector Hints --- .../Decorator/InspectorHints.php | 31 ------------------- 1 file changed, 31 deletions(-) diff --git a/src/Model/TemplateEngine/Decorator/InspectorHints.php b/src/Model/TemplateEngine/Decorator/InspectorHints.php index 9d43399..a2fa4ba 100644 --- a/src/Model/TemplateEngine/Decorator/InspectorHints.php +++ b/src/Model/TemplateEngine/Decorator/InspectorHints.php @@ -97,20 +97,6 @@ private function isExcludedTemplate(string $templateFile): bool return false; } - /** - * Check if rendered HTML contains wire attributes (Magewire/Livewire components) - * - * Wrapping these in HTML comments breaks wire:id injection which relies on - * finding the first root element via regex. - * - * @param string $html - * @return bool - */ - private function containsWireAttributes(string $html): bool - { - return str_contains($html, 'wire:id=') || str_contains($html, 'wire:initial-data='); - } - /** * Insert inspector data attributes into the rendered block contents * @@ -137,27 +123,10 @@ public function render(BlockInterface $block, $templateFile, array $dictionary = } // Skip inspector wrapping for templates in excluded paths (e.g. /magewire/ directories). - // Magewire injects wire:id AFTER the template engine returns via regex on the root element. - // Wrapping the output in HTML comments before that element breaks the injection. if ($this->isExcludedTemplate($templateFile)) { return $result; } - // Skip inspector wrapping for Magewire component blocks. - // Magewire sets a 'magewire' data key on the block before rendering and injects wire:id - // via regex AFTER the template engine returns. Wrapping the output in HTML comments - // shifts the offset used by insertAttributesIntoHtmlRoot(), causing broken components. - // Soft dependency: hasData() is a Magento DataObject method, not a Magewire class. - if (method_exists($block, 'hasData') && $block->hasData('magewire')) { - return $result; - } - - // Skip inspector wrapping if the rendered HTML contains wire attributes (Magewire/Livewire). - // This catches container blocks whose children have already been rendered with wire attributes. - if ($this->containsWireAttributes($result)) { - return $result; - } - // Only inject attributes if there's actual HTML content if (empty(trim($result))) { return $result; From 41a895f44097e0d8122201f72155dfe16485819f Mon Sep 17 00:00:00 2001 From: Mathias Elle Date: Wed, 27 May 2026 17:42:08 +0200 Subject: [PATCH 05/13] fix: simplify warning display logic in CompatibilityChecker --- src/Service/Hyva/CompatibilityChecker.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Service/Hyva/CompatibilityChecker.php b/src/Service/Hyva/CompatibilityChecker.php index d513639..55f979f 100644 --- a/src/Service/Hyva/CompatibilityChecker.php +++ b/src/Service/Hyva/CompatibilityChecker.php @@ -203,7 +203,7 @@ private function getStatusDisplay(array $moduleData): string return '✓ Compatible'; } - if ($moduleData['compatible'] && $moduleData['hasWarnings']) { + if ($moduleData['compatible']) { return '⚠ Warnings'; } From fc4b7ee8f6ae90a0d9d560c727339b6f56b83db8 Mon Sep 17 00:00:00 2001 From: Mathias Elle Date: Fri, 29 May 2026 12:34:58 +0200 Subject: [PATCH 06/13] feat: add new color group styles to toolbar (#188) * feat: add new color variables and group styles to toolbar CSS; update createMenuItem to accept groupKey * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * fix: scope purple variable and increase hover specificity for group labels * fix: simplify warning display logic in CompatibilityChecker * fix: rename purple color variable and update groupKey data attribute in toolbar --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/view/frontend/web/css/toolbar.css | 34 +++++++++++++++++++++++--- src/view/frontend/web/js/toolbar/ui.js | 7 ++++-- 2 files changed, 36 insertions(+), 5 deletions(-) diff --git a/src/view/frontend/web/css/toolbar.css b/src/view/frontend/web/css/toolbar.css index d5323d5..ede922a 100644 --- a/src/view/frontend/web/css/toolbar.css +++ b/src/view/frontend/web/css/toolbar.css @@ -22,7 +22,11 @@ --mageforge-color-slate-400: #94a3b8; --mageforge-color-orange: #fb923c; --mageforge-color-pink: #C850C0; + --mageforge-color-purple: #a855f7; --mageforge-color-amber: #edb04d; + --mageforge-group-color-wcag: var(--mageforge-color-purple); + --mageforge-group-color-html-quality: var(--mageforge-color-blue); + --mageforge-group-color-performance: var(--mageforge-color-orange); --mageforge-color-amber-alpha-15: rgba(237, 176, 77, 0.15); --mageforge-color-amber-alpha-35: rgba(237, 176, 77, 0.35); --mageforge-bg-dark: rgba(15, 23, 42, 0.98); @@ -332,7 +336,7 @@ font-size: 11px; line-height: 1.3; user-select: text; - cursor: text; + cursor: default; } .mageforge-toolbar-menu-desc.mageforge-active { @@ -405,7 +409,7 @@ margin-bottom: 4px; } -.mageforge-toolbar-menu-group-header:hover .mageforge-toolbar-menu-group-label, +.mageforge-toolbar-menu-group .mageforge-toolbar-menu-group-header:hover .mageforge-toolbar-menu-group-label, .mageforge-toolbar-menu-group-header:hover .mageforge-toolbar-menu-group-chevron { color: var(--mageforge-color-white); } @@ -417,7 +421,31 @@ text-transform: uppercase; transition: color 0.15s ease; letter-spacing: 0.08em; - color: var(--mageforge-color-orange); + color: var(--mageforge-color-slate-400); +} + +.mageforge-toolbar-menu-group[data-group-key="wcag"] .mageforge-toolbar-menu-group-label { + color: var(--mageforge-group-color-wcag); +} + +.mageforge-toolbar-menu-group[data-group-key="html-quality"] .mageforge-toolbar-menu-group-label { + color: var(--mageforge-group-color-html-quality); +} + +.mageforge-toolbar-menu-group[data-group-key="performance"] .mageforge-toolbar-menu-group-label { + color: var(--mageforge-group-color-performance); +} + +.mageforge-toolbar-menu-item[data-group-key="wcag"] .mageforge-toolbar-menu-icon { + color: var(--mageforge-group-color-wcag); +} + +.mageforge-toolbar-menu-item[data-group-key="html-quality"] .mageforge-toolbar-menu-icon { + color: var(--mageforge-group-color-html-quality); +} + +.mageforge-toolbar-menu-item[data-group-key="performance"] .mageforge-toolbar-menu-icon { + color: var(--mageforge-group-color-performance); } .mageforge-toolbar-menu-group-chevron { diff --git a/src/view/frontend/web/js/toolbar/ui.js b/src/view/frontend/web/js/toolbar/ui.js index 23e50e8..157bae9 100644 --- a/src/view/frontend/web/js/toolbar/ui.js +++ b/src/view/frontend/web/js/toolbar/ui.js @@ -264,7 +264,8 @@ export const uiMethods = { audit.icon, audit.label, audit.description, - () => this.runAudit(audit.key) + () => this.runAudit(audit.key), + key )); }); @@ -280,13 +281,15 @@ export const uiMethods = { * @param {string} label * @param {string} description * @param {Function} callback + * @param {?string} groupKey - Optional parent group key for the item * @return {HTMLButtonElement} */ - createMenuItem(key, icon, label, description, callback) { + createMenuItem(key, icon, label, description, callback, groupKey = null) { const item = document.createElement('button'); item.type = 'button'; item.className = 'mageforge-toolbar-menu-item'; item.dataset.auditKey = key; + if (groupKey) item.dataset.groupKey = groupKey; item.setAttribute('aria-pressed', 'false'); const iconEl = document.createElement('span'); From 3a2726230749ba25c56c13d102c763e42ff68c4d Mon Sep 17 00:00:00 2001 From: Mathias Elle Date: Mon, 1 Jun 2026 09:56:44 +0200 Subject: [PATCH 07/13] feat: implement InspectorHintsFactory for creating InspectorHints instances --- .../Decorator/InspectorHintsFactory.php | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 src/Model/TemplateEngine/Decorator/InspectorHintsFactory.php diff --git a/src/Model/TemplateEngine/Decorator/InspectorHintsFactory.php b/src/Model/TemplateEngine/Decorator/InspectorHintsFactory.php new file mode 100644 index 0000000..021bb01 --- /dev/null +++ b/src/Model/TemplateEngine/Decorator/InspectorHintsFactory.php @@ -0,0 +1,24 @@ + $data + */ + public function create(array $data = []): InspectorHints + { + /** @var InspectorHints $instance */ + $instance = $this->objectManager->create(InspectorHints::class, $data); + return $instance; + } +} From 68b6beacdf6743f2bd26addbdfc108a21d4a8ff4 Mon Sep 17 00:00:00 2001 From: Mathias Elle Date: Mon, 1 Jun 2026 11:00:07 +0200 Subject: [PATCH 08/13] feat: improve accessibility and styling for inspector and toolbar buttons --- src/view/frontend/web/css/inspector.css | 7 ++- src/view/frontend/web/js/inspector.js | 14 +++++- src/view/frontend/web/js/toolbar/ui.js | 61 +++++++++++++++++++------ 3 files changed, 64 insertions(+), 18 deletions(-) diff --git a/src/view/frontend/web/css/inspector.css b/src/view/frontend/web/css/inspector.css index b9d9464..ac4db1e 100644 --- a/src/view/frontend/web/css/inspector.css +++ b/src/view/frontend/web/css/inspector.css @@ -92,12 +92,17 @@ align-items: center; gap: 8px; font-family: var(--mageforge-font-family); - font-size: 0.75rem; + font-size: 14px; font-weight: 600; + line-height: 1; + white-space: nowrap; + text-transform: none; box-shadow: 0 4px 12px rgba(59, 130, 246, 0.4), 0 2px 4px rgba(0, 0, 0, 0.2); transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); backdrop-filter: blur(8px); letter-spacing: 0.025em; + vertical-align: middle; + text-decoration: none; } .mageforge-inspector-float-button:hover { diff --git a/src/view/frontend/web/js/inspector.js b/src/view/frontend/web/js/inspector.js index 3932f7a..3e848b5 100644 --- a/src/view/frontend/web/js/inspector.js +++ b/src/view/frontend/web/js/inspector.js @@ -109,6 +109,7 @@ function _registerMageforgeInspector() { this._inspectorStateHandler = (e) => { if (this._inspectorFloatButton) { this._inspectorFloatButton.classList.toggle('mageforge-active', e.detail.active); + this._inspectorFloatButton.setAttribute('aria-pressed', e.detail.active ? 'true' : 'false'); } }; window.addEventListener('mageforge:toolbar:inspector-state', this._inspectorStateHandler); @@ -123,10 +124,13 @@ function _registerMageforgeInspector() { }, _createInspectorFloatButton() { - const btn = document.createElement('button'); + // Use div instead of button to avoid Luma/theme button CSS overrides + const btn = document.createElement('div'); btn.className = 'mageforge-inspector-float-button'; - btn.type = 'button'; btn.title = 'Activate Inspector (Ctrl+Shift+I)'; + btn.setAttribute('role', 'button'); + btn.setAttribute('tabindex', '0'); + btn.setAttribute('aria-pressed', 'false'); btn.innerHTML = ` @@ -142,6 +146,12 @@ function _registerMageforgeInspector() { e.stopPropagation(); this.toggleInspector(); }; + btn.onkeydown = (e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + this.toggleInspector(); + } + }; return btn; }, diff --git a/src/view/frontend/web/js/toolbar/ui.js b/src/view/frontend/web/js/toolbar/ui.js index 157bae9..405fcc9 100644 --- a/src/view/frontend/web/js/toolbar/ui.js +++ b/src/view/frontend/web/js/toolbar/ui.js @@ -117,8 +117,9 @@ export const uiMethods = { const buttonRow = document.createElement('div'); buttonRow.className = 'mageforge-toolbar-menu-button-row'; - this.runAllButton = document.createElement('button'); - this.runAllButton.type = 'button'; + this.runAllButton = document.createElement('div'); + this.runAllButton.setAttribute('role', 'button'); + this.runAllButton.setAttribute('tabindex', '0'); this.runAllButton.className = 'mageforge-toolbar-menu-run-all'; this.runAllButton.innerHTML = ` @@ -128,10 +129,14 @@ export const uiMethods = { e.stopPropagation(); this.runAllAuditsForScore(); }; + this.runAllButton.onkeydown = (e) => { + if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); this.runAllAuditsForScore(); } + }; buttonRow.appendChild(this.runAllButton); - this.resetButton = document.createElement('button'); - this.resetButton.type = 'button'; + this.resetButton = document.createElement('div'); + this.resetButton.setAttribute('role', 'button'); + this.resetButton.setAttribute('tabindex', '0'); this.resetButton.className = 'mageforge-toolbar-menu-reset'; this.resetButton.title = 'Reset score and deactivate all audits'; this.resetButton.setAttribute('aria-label', 'Reset score and deactivate all audits'); @@ -140,6 +145,9 @@ export const uiMethods = { e.stopPropagation(); this.resetScore(); }; + this.resetButton.onkeydown = (e) => { + if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); this.resetScore(); } + }; buttonRow.appendChild(this.resetButton); menuFooter.appendChild(buttonRow); @@ -148,8 +156,9 @@ export const uiMethods = { const buttonRow = document.createElement('div'); buttonRow.className = 'mageforge-toolbar-menu-button-row'; - this.runAllButton = document.createElement('button'); - this.runAllButton.type = 'button'; + this.runAllButton = document.createElement('div'); + this.runAllButton.setAttribute('role', 'button'); + this.runAllButton.setAttribute('tabindex', '0'); this.runAllButton.className = 'mageforge-toolbar-menu-run-all'; this.runAllButton.innerHTML = ` @@ -159,10 +168,14 @@ export const uiMethods = { e.stopPropagation(); this.runAllAuditsForScore(); }; + this.runAllButton.onkeydown = (e) => { + if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); this.runAllAuditsForScore(); } + }; buttonRow.appendChild(this.runAllButton); - this.resetButton = document.createElement('button'); - this.resetButton.type = 'button'; + this.resetButton = document.createElement('div'); + this.resetButton.setAttribute('role', 'button'); + this.resetButton.setAttribute('tabindex', '0'); this.resetButton.className = 'mageforge-toolbar-menu-reset'; this.resetButton.title = 'Deactivate all audits'; this.resetButton.setAttribute('aria-label', 'Deactivate all audits'); @@ -171,6 +184,9 @@ export const uiMethods = { e.stopPropagation(); this.resetScore(); }; + this.resetButton.onkeydown = (e) => { + if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); this.resetScore(); } + }; buttonRow.appendChild(this.resetButton); menuFooter.appendChild(buttonRow); @@ -182,11 +198,12 @@ export const uiMethods = { menuFooter.appendChild(credit); this.menu.appendChild(menuFooter); - // Burger button (left) - this.burgerButton = document.createElement('button'); + // Burger button (left) — div avoids Luma/theme button CSS overrides + this.burgerButton = document.createElement('div'); this.burgerButton.className = 'mageforge-toolbar-burger'; - this.burgerButton.type = 'button'; this.burgerButton.title = 'Audit tools'; + this.burgerButton.setAttribute('role', 'button'); + this.burgerButton.setAttribute('tabindex', '0'); this.burgerButton.setAttribute('aria-label', 'MageForge Toolbar'); this.burgerButton.setAttribute('aria-expanded', 'false'); this.burgerButton.innerHTML = ` @@ -202,6 +219,12 @@ export const uiMethods = { e.stopPropagation(); this.toggleMenu(); }; + this.burgerButton.onkeydown = (e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + this.toggleMenu(); + } + }; this.container.appendChild(this.menu); this.container.appendChild(this.burgerButton); @@ -231,8 +254,9 @@ export const uiMethods = { group.className = 'mageforge-toolbar-menu-group'; group.dataset.groupKey = key; - const header = document.createElement('button'); - header.type = 'button'; + const header = document.createElement('div'); + header.setAttribute('role', 'button'); + header.setAttribute('tabindex', '0'); header.className = 'mageforge-toolbar-menu-group-header'; header.setAttribute('aria-expanded', String(!this.collapsedGroups.has(key))); header.onclick = (e) => { @@ -240,6 +264,9 @@ export const uiMethods = { e.stopPropagation(); this.toggleGroup(key); }; + header.onkeydown = (e) => { + if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); this.toggleGroup(key); } + }; const headerLabel = document.createElement('span'); headerLabel.className = 'mageforge-toolbar-menu-group-label'; @@ -285,8 +312,9 @@ export const uiMethods = { * @return {HTMLButtonElement} */ createMenuItem(key, icon, label, description, callback, groupKey = null) { - const item = document.createElement('button'); - item.type = 'button'; + const item = document.createElement('div'); + item.setAttribute('role', 'button'); + item.setAttribute('tabindex', '0'); item.className = 'mageforge-toolbar-menu-item'; item.dataset.auditKey = key; if (groupKey) item.dataset.groupKey = groupKey; @@ -334,6 +362,9 @@ export const uiMethods = { e.stopPropagation(); callback(); }; + item.onkeydown = (e) => { + if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); callback(); } + }; return item; }, From adaa5db94326136ebaa9269ffde1a9b58a0ca3f9 Mon Sep 17 00:00:00 2001 From: Mathias Elle Date: Mon, 1 Jun 2026 11:00:19 +0200 Subject: [PATCH 09/13] feat: add Escaper dependency and improve JSON escaping in InspectorHints --- src/Model/TemplateEngine/Decorator/InspectorHints.php | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/Model/TemplateEngine/Decorator/InspectorHints.php b/src/Model/TemplateEngine/Decorator/InspectorHints.php index a2fa4ba..40b0843 100644 --- a/src/Model/TemplateEngine/Decorator/InspectorHints.php +++ b/src/Model/TemplateEngine/Decorator/InspectorHints.php @@ -4,6 +4,7 @@ namespace OpenForgeProject\MageForge\Model\TemplateEngine\Decorator; +use Magento\Framework\Escaper; use Magento\Framework\Filesystem\Driver\File; use Magento\Framework\Math\Random; use Magento\Framework\View\Element\AbstractBlock; @@ -29,6 +30,7 @@ class InspectorHints implements TemplateEngineInterface * @param Random $random * @param BlockCacheCollector $cacheCollector * @param File $fileDriver + * @param Escaper $escaper * @param string[] $excludedClassPrefixes Block class prefixes to skip inspector wrapping for * @param string[] $excludedTemplatePaths Template path substrings to skip inspector wrapping for */ @@ -38,6 +40,7 @@ public function __construct( private readonly Random $random, private readonly BlockCacheCollector $cacheCollector, private readonly File $fileDriver, + private readonly Escaper $escaper, private readonly array $excludedClassPrefixes = [], private readonly array $excludedTemplatePaths = [], ) { @@ -212,10 +215,11 @@ private function injectInspectorAttributes( return $html; } - // Escape single quotes so JSON can be safely embedded in a single-quoted HTML attribute. + // Escape all characters that need HTML-encoding so the JSON can be safely + // embedded in an HTML attribute. escapeHtml handles &, <, > and quotes. // The browser automatically decodes HTML entities when getAttribute() is called, // so JSON.parse() on the JS side will receive the correct string. - $safeJson = str_replace("'", ''', $jsonMetadata); + $safeJson = $this->escaper->escapeHtml($jsonMetadata); // Inject data-mageforge-* attributes on the first root HTML element. // This avoids HTML comment nodes which corrupt markup when block output is @@ -227,7 +231,7 @@ function (array $matches) use ($wrapperId, $safeJson, &$replaced): string { $replaced = true; return $matches[0] . ' data-mageforge-id="' . $wrapperId . '"' - . ' data-mageforge-block=\'' . $safeJson . "'"; + . ' data-mageforge-block="' . $safeJson . '"'; }, $html, 1, From 303955a25142d73e63d5c7789479ea17bbfb92a6 Mon Sep 17 00:00:00 2001 From: Mathias Elle Date: Mon, 1 Jun 2026 11:10:01 +0200 Subject: [PATCH 10/13] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/view/frontend/web/js/toolbar/ui.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/view/frontend/web/js/toolbar/ui.js b/src/view/frontend/web/js/toolbar/ui.js index 405fcc9..6778f4d 100644 --- a/src/view/frontend/web/js/toolbar/ui.js +++ b/src/view/frontend/web/js/toolbar/ui.js @@ -309,7 +309,7 @@ export const uiMethods = { * @param {string} description * @param {Function} callback * @param {?string} groupKey - Optional parent group key for the item - * @return {HTMLButtonElement} + * @return {HTMLDivElement} */ createMenuItem(key, icon, label, description, callback, groupKey = null) { const item = document.createElement('div'); From 57a0263144329bd8e29f61ce8e2d894d534ec94e Mon Sep 17 00:00:00 2001 From: Mathias Elle Date: Mon, 1 Jun 2026 11:36:07 +0200 Subject: [PATCH 11/13] feat: update CompatibilityCheckCommand to handle summary directly --- src/Console/Command/Hyva/CompatibilityCheckCommand.php | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/Console/Command/Hyva/CompatibilityCheckCommand.php b/src/Console/Command/Hyva/CompatibilityCheckCommand.php index 4cd297e..fa6436f 100644 --- a/src/Console/Command/Hyva/CompatibilityCheckCommand.php +++ b/src/Console/Command/Hyva/CompatibilityCheckCommand.php @@ -257,7 +257,7 @@ private function runScan( } // Display summary - $this->displaySummary($results); + $this->displaySummary($results['summary']); // Display recommendations if there are issues if ($results['hasIncompatibilities']) { @@ -336,13 +336,11 @@ private function displayDetailedIssues(array $results): void /** * Display summary statistics * - * @param array $results - * @phpstan-param array{summary: CheckSummary} $results + * @param array $summary + * @phpstan-param CheckSummary $summary */ - private function displaySummary(array $results): void + private function displaySummary(array $summary): void { - $summary = $results['summary']; - $this->io->section('Summary'); $summaryData = [ From 287397062c19afa157f8e7b486319db7fca62e39 Mon Sep 17 00:00:00 2001 From: Mathias Elle Date: Mon, 1 Jun 2026 11:50:49 +0200 Subject: [PATCH 12/13] feat: improve accessibility and functionality of inspector and toolbar components --- .../Decorator/InspectorHints.php | 2 +- .../Decorator/InspectorHintsFactory.php | 12 ++++- src/view/frontend/web/css/inspector.css | 5 ++ src/view/frontend/web/js/inspector.js | 16 +++++-- src/view/frontend/web/js/inspector/dom.js | 2 +- src/view/frontend/web/js/inspector/ui.js | 13 +++-- src/view/frontend/web/js/toolbar.js | 6 +-- src/view/frontend/web/js/toolbar/ui.js | 48 ++++++++++++++++--- 8 files changed, 83 insertions(+), 21 deletions(-) diff --git a/src/Model/TemplateEngine/Decorator/InspectorHints.php b/src/Model/TemplateEngine/Decorator/InspectorHints.php index 40b0843..935c415 100644 --- a/src/Model/TemplateEngine/Decorator/InspectorHints.php +++ b/src/Model/TemplateEngine/Decorator/InspectorHints.php @@ -226,7 +226,7 @@ private function injectInspectorAttributes( // embedded inside HTML attribute values (e.g. PageBuilder URL blocks in href="..."). $replaced = false; $result = preg_replace_callback( - '/^(\s*<[a-zA-Z][a-zA-Z0-9]*)/s', + '/^(\s*<[a-zA-Z][a-zA-Z0-9-]*)/s', function (array $matches) use ($wrapperId, $safeJson, &$replaced): string { $replaced = true; return $matches[0] diff --git a/src/Model/TemplateEngine/Decorator/InspectorHintsFactory.php b/src/Model/TemplateEngine/Decorator/InspectorHintsFactory.php index 021bb01..e6205e3 100644 --- a/src/Model/TemplateEngine/Decorator/InspectorHintsFactory.php +++ b/src/Model/TemplateEngine/Decorator/InspectorHintsFactory.php @@ -6,14 +6,24 @@ use Magento\Framework\ObjectManagerInterface; +/** + * Factory for creating InspectorHints instances via the ObjectManager. + */ class InspectorHintsFactory { + /** + * @param ObjectManagerInterface $objectManager + */ public function __construct( private readonly ObjectManagerInterface $objectManager, - ) {} + ) { + } /** + * Create a new InspectorHints instance. + * * @param array $data + * @return InspectorHints */ public function create(array $data = []): InspectorHints { diff --git a/src/view/frontend/web/css/inspector.css b/src/view/frontend/web/css/inspector.css index ac4db1e..2587f2f 100644 --- a/src/view/frontend/web/css/inspector.css +++ b/src/view/frontend/web/css/inspector.css @@ -114,6 +114,11 @@ transform: translateY(0); } +.mageforge-inspector-float-button:focus-visible { + outline: 2px solid rgba(255, 255, 255, 0.8); + outline-offset: 2px; +} + .mageforge-inspector-float-button.mageforge-active { background: linear-gradient(135deg, var(--mageforge-color-green) 0%, var(--mageforge-color-green-dark) 100%); box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.2), 0 8px 20px rgba(16, 185, 129, 0.5); diff --git a/src/view/frontend/web/js/inspector.js b/src/view/frontend/web/js/inspector.js index 3e848b5..3e2aeea 100644 --- a/src/view/frontend/web/js/inspector.js +++ b/src/view/frontend/web/js/inspector.js @@ -76,10 +76,6 @@ function _registerMageforgeInspector() { pageTimings: null, performanceObservers: [], - // Block detection cache - cachedBlocks: null, - lastBlocksCacheTime: 0, - // Window event handler refs (for cleanup) _inspectorStateHandler: null, @@ -147,8 +143,18 @@ function _registerMageforgeInspector() { this.toggleInspector(); }; btn.onkeydown = (e) => { - if (e.key === 'Enter' || e.key === ' ') { + if (e.key === 'Enter') { e.preventDefault(); + e.stopPropagation(); + this.toggleInspector(); + } + if (e.key === ' ') { + e.preventDefault(); // prevent page scroll on Space + } + }; + btn.onkeyup = (e) => { + if (e.key === ' ') { + e.stopPropagation(); this.toggleInspector(); } }; diff --git a/src/view/frontend/web/js/inspector/dom.js b/src/view/frontend/web/js/inspector/dom.js index 4e64a7a..68f1dc8 100644 --- a/src/view/frontend/web/js/inspector/dom.js +++ b/src/view/frontend/web/js/inspector/dom.js @@ -39,7 +39,7 @@ export const domMethods = { data.id = el.getAttribute('data-mageforge-id'); return { data, - elements: [el, ...el.querySelectorAll('*')], + elements: [el], }; } catch (e) { console.error('Failed to parse MageForge block data:', e); diff --git a/src/view/frontend/web/js/inspector/ui.js b/src/view/frontend/web/js/inspector/ui.js index 1daa1a6..8421b74 100644 --- a/src/view/frontend/web/js/inspector/ui.js +++ b/src/view/frontend/web/js/inspector/ui.js @@ -98,10 +98,17 @@ export const uiMethods = { const rect = this.getElementRect(element); const elementId = element.getAttribute('data-mageforge-id'); - // Only rebuild badge content if it's a different element - if (this.infoBadge.dataset.currentElement !== elementId) { + // Only rebuild badge content if it's a different element. + // For PageBuilder fallback elements that lack data-mageforge-id, use element + // reference comparison to avoid rebuilding on every hover/click. + const isSame = elementId !== null + ? this.infoBadge.dataset.currentElement === elementId + : this.infoBadge._currentElement === element; + + if (!isSame) { this.buildBadgeContent(element); - this.infoBadge.dataset.currentElement = elementId; + this.infoBadge.dataset.currentElement = elementId ?? ''; + this.infoBadge._currentElement = element; } this.positionBadge(rect); diff --git a/src/view/frontend/web/js/toolbar.js b/src/view/frontend/web/js/toolbar.js index 0162dd0..b857a63 100644 --- a/src/view/frontend/web/js/toolbar.js +++ b/src/view/frontend/web/js/toolbar.js @@ -21,16 +21,16 @@ function _registerMageforgeToolbar() { /** @type {HTMLDivElement|null} */ container: null, - /** @type {HTMLButtonElement|null} */ + /** @type {HTMLDivElement|null} */ burgerButton: null, /** @type {HTMLDivElement|null} */ menu: null, - /** @type {HTMLButtonElement|null} */ + /** @type {HTMLDivElement|null} */ runAllButton: null, - /** @type {HTMLButtonElement|null} */ + /** @type {HTMLDivElement|null} */ resetButton: null, // ==================================================================== diff --git a/src/view/frontend/web/js/toolbar/ui.js b/src/view/frontend/web/js/toolbar/ui.js index 6778f4d..f8ea8c0 100644 --- a/src/view/frontend/web/js/toolbar/ui.js +++ b/src/view/frontend/web/js/toolbar/ui.js @@ -130,7 +130,11 @@ export const uiMethods = { this.runAllAuditsForScore(); }; this.runAllButton.onkeydown = (e) => { - if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); this.runAllAuditsForScore(); } + if (e.key === 'Enter') { e.preventDefault(); e.stopPropagation(); this.runAllAuditsForScore(); } + if (e.key === ' ') { e.preventDefault(); } + }; + this.runAllButton.onkeyup = (e) => { + if (e.key === ' ') { e.stopPropagation(); this.runAllAuditsForScore(); } }; buttonRow.appendChild(this.runAllButton); @@ -146,7 +150,11 @@ export const uiMethods = { this.resetScore(); }; this.resetButton.onkeydown = (e) => { - if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); this.resetScore(); } + if (e.key === 'Enter') { e.preventDefault(); e.stopPropagation(); this.resetScore(); } + if (e.key === ' ') { e.preventDefault(); } + }; + this.resetButton.onkeyup = (e) => { + if (e.key === ' ') { e.stopPropagation(); this.resetScore(); } }; buttonRow.appendChild(this.resetButton); @@ -169,7 +177,11 @@ export const uiMethods = { this.runAllAuditsForScore(); }; this.runAllButton.onkeydown = (e) => { - if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); this.runAllAuditsForScore(); } + if (e.key === 'Enter') { e.preventDefault(); e.stopPropagation(); this.runAllAuditsForScore(); } + if (e.key === ' ') { e.preventDefault(); } + }; + this.runAllButton.onkeyup = (e) => { + if (e.key === ' ') { e.stopPropagation(); this.runAllAuditsForScore(); } }; buttonRow.appendChild(this.runAllButton); @@ -185,7 +197,11 @@ export const uiMethods = { this.resetScore(); }; this.resetButton.onkeydown = (e) => { - if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); this.resetScore(); } + if (e.key === 'Enter') { e.preventDefault(); e.stopPropagation(); this.resetScore(); } + if (e.key === ' ') { e.preventDefault(); } + }; + this.resetButton.onkeyup = (e) => { + if (e.key === ' ') { e.stopPropagation(); this.resetScore(); } }; buttonRow.appendChild(this.resetButton); @@ -220,8 +236,18 @@ export const uiMethods = { this.toggleMenu(); }; this.burgerButton.onkeydown = (e) => { - if (e.key === 'Enter' || e.key === ' ') { + if (e.key === 'Enter') { e.preventDefault(); + e.stopPropagation(); + this.toggleMenu(); + } + if (e.key === ' ') { + e.preventDefault(); // prevent page scroll + } + }; + this.burgerButton.onkeyup = (e) => { + if (e.key === ' ') { + e.stopPropagation(); this.toggleMenu(); } }; @@ -265,7 +291,11 @@ export const uiMethods = { this.toggleGroup(key); }; header.onkeydown = (e) => { - if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); this.toggleGroup(key); } + if (e.key === 'Enter') { e.preventDefault(); e.stopPropagation(); this.toggleGroup(key); } + if (e.key === ' ') { e.preventDefault(); } + }; + header.onkeyup = (e) => { + if (e.key === ' ') { e.stopPropagation(); this.toggleGroup(key); } }; const headerLabel = document.createElement('span'); @@ -363,7 +393,11 @@ export const uiMethods = { callback(); }; item.onkeydown = (e) => { - if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); callback(); } + if (e.key === 'Enter') { e.preventDefault(); e.stopPropagation(); callback(); } + if (e.key === ' ') { e.preventDefault(); } + }; + item.onkeyup = (e) => { + if (e.key === ' ') { e.stopPropagation(); callback(); } }; return item; }, From e8c2ee3d3d4efd98af21b2b85be1c465cd340b4a Mon Sep 17 00:00:00 2001 From: Mathias Elle Date: Mon, 1 Jun 2026 11:55:29 +0200 Subject: [PATCH 13/13] feat: enable phpcs annotations for method arguments in InspectorHintsFactory --- src/Model/TemplateEngine/Decorator/InspectorHintsFactory.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Model/TemplateEngine/Decorator/InspectorHintsFactory.php b/src/Model/TemplateEngine/Decorator/InspectorHintsFactory.php index e6205e3..2e35f98 100644 --- a/src/Model/TemplateEngine/Decorator/InspectorHintsFactory.php +++ b/src/Model/TemplateEngine/Decorator/InspectorHintsFactory.php @@ -19,12 +19,14 @@ public function __construct( ) { } + // phpcs:disable Magento2.Annotation.MethodArguments.ArgumentMissing /** * Create a new InspectorHints instance. * * @param array $data * @return InspectorHints */ + // phpcs:enable Magento2.Annotation.MethodArguments.ArgumentMissing public function create(array $data = []): InspectorHints { /** @var InspectorHints $instance */