From 98abef416b89ad0629fca78293e7c992eb26bbd1 Mon Sep 17 00:00:00 2001 From: Mathias Elle Date: Tue, 16 Jun 2026 17:24:01 +0200 Subject: [PATCH 01/48] feat: Add MageForge Toolbar styles and animations for various components --- src/view/frontend/web/css/toolbar.css | 900 +----------------- .../frontend/web/css/toolbar/_animations.css | 28 + src/view/frontend/web/css/toolbar/_burger.css | 86 ++ .../frontend/web/css/toolbar/_buttons.css | 83 ++ src/view/frontend/web/css/toolbar/_credit.css | 30 + .../frontend/web/css/toolbar/_feedback.css | 38 + src/view/frontend/web/css/toolbar/_footer.css | 14 + src/view/frontend/web/css/toolbar/_groups.css | 93 ++ src/view/frontend/web/css/toolbar/_health.css | 61 ++ .../frontend/web/css/toolbar/_highlights.css | 23 + src/view/frontend/web/css/toolbar/_menu.css | 307 ++++++ .../frontend/web/css/toolbar/_positions.css | 49 + src/view/frontend/web/css/toolbar/_reset.css | 16 + .../frontend/web/css/toolbar/_responsive.css | 42 + src/view/frontend/web/css/toolbar/_themes.css | 52 + .../frontend/web/css/toolbar/_variables.css | 73 ++ 16 files changed, 1013 insertions(+), 882 deletions(-) create mode 100644 src/view/frontend/web/css/toolbar/_animations.css create mode 100644 src/view/frontend/web/css/toolbar/_burger.css create mode 100644 src/view/frontend/web/css/toolbar/_buttons.css create mode 100644 src/view/frontend/web/css/toolbar/_credit.css create mode 100644 src/view/frontend/web/css/toolbar/_feedback.css create mode 100644 src/view/frontend/web/css/toolbar/_footer.css create mode 100644 src/view/frontend/web/css/toolbar/_groups.css create mode 100644 src/view/frontend/web/css/toolbar/_health.css create mode 100644 src/view/frontend/web/css/toolbar/_highlights.css create mode 100644 src/view/frontend/web/css/toolbar/_menu.css create mode 100644 src/view/frontend/web/css/toolbar/_positions.css create mode 100644 src/view/frontend/web/css/toolbar/_reset.css create mode 100644 src/view/frontend/web/css/toolbar/_responsive.css create mode 100644 src/view/frontend/web/css/toolbar/_themes.css create mode 100644 src/view/frontend/web/css/toolbar/_variables.css diff --git a/src/view/frontend/web/css/toolbar.css b/src/view/frontend/web/css/toolbar.css index 527aedc..d8df23d 100644 --- a/src/view/frontend/web/css/toolbar.css +++ b/src/view/frontend/web/css/toolbar.css @@ -1,889 +1,25 @@ /** - * MageForge Toolbar - Vanilla CSS Styles + * MageForge Toolbar - Main Stylesheet * - * Standalone styles for the MageForge toolbar feature. - * Completely independent from the Inspector CSS. - * All classes prefixed with 'mageforge-toolbar' for namespace isolation. + * Imports all toolbar CSS modules in dependency order. + * This file is the single entry point loaded by Magento 2. * * @package OpenForgeProject\MageForge * @license GPL-3.0 */ -:root { - --mageforge-font-family: - "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, - "Helvetica Neue", Arial, sans-serif; - --mageforge-color-white: #ffffff; - --mageforge-color-blue: #3b82f6; - --mageforge-color-green: #10b981; - --mageforge-color-green-alpha-15: rgba(16, 185, 129, 0.15); - --mageforge-color-green-alpha-35: rgba(16, 185, 129, 0.35); - --mageforge-color-red: #ef4444; - --mageforge-color-red-alpha-15: rgba(239, 68, 68, 0.15); - --mageforge-color-red-alpha-35: rgba(239, 68, 68, 0.35); - --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); - --mageforge-bg-dark-alt: rgba(30, 41, 59, 0.98); - --mageforge-border-color: rgba(148, 163, 184, 0.15); - --mageforge-surface-glass-hover: rgba(255, 255, 255, 0.08); - --mageforge-border-glass: rgba(255, 255, 255, 0.1); - --mageforge-shadow-sm: rgba(0, 0, 0, 0.2); - --mageforge-shadow-md: rgba(0, 0, 0, 0.3); - --mageforge-shadow-lg: rgba(0, 0, 0, 0.4); - --mageforge-burger-bg: rgba(15, 23, 42, 0.85); - --mageforge-burger-bg-hover: rgba(30, 41, 59, 0.95); - --mageforge-toggle-bg-off: rgba(255, 255, 255, 0.15); - --mageforge-toggle-bg-off-light: rgba(0, 0, 0, 0.15); - --gradient-brand: linear-gradient( - 43deg, - var(--mageforge-color-blue) 0%, - var(--mageforge-color-pink) 50%, - var(--mageforge-color-amber) 100% - ); - --gradient-brand-hover: linear-gradient( - 43deg, - var(--mageforge-color-blue) 0%, - var(--mageforge-color-pink) 70%, - var(--mageforge-color-amber) 100% - ); -} - -.mageforge-toolbar *, -.mageforge-toolbar *::before, -.mageforge-toolbar *::after { - box-sizing: border-box; - margin: 0; - padding: 0; -} - -/* ============================================================================ - Toolbar Container - ========================================================================== */ - -.mageforge-toolbar { - position: fixed; - bottom: 16px; - left: 16px; - display: flex; - align-items: center; - gap: 8px; - z-index: 9999998; - pointer-events: auto; -} - -/* ============================================================================ - Burger Button - ========================================================================== */ - -.mageforge-toolbar-burger { - height: 36px; - padding: 0 10px 0 8px; - background: var(--mageforge-burger-bg); - color: var(--mageforge-color-white); - border-radius: 5px; - cursor: pointer; - display: flex; - flex-direction: row; - align-items: center; - justify-content: center; - gap: 8px; - transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); - box-shadow: - 0 4px 12px var(--mageforge-shadow-md), - 0 2px 4px var(--mageforge-shadow-sm); - flex-shrink: 0; - background-image: var(--gradient-brand); -} - -.mageforge-toolbar-burger:hover { - background: var(--mageforge-burger-bg-hover); - transform: translateY(-2px); - box-shadow: - 0 8px 20px var(--mageforge-shadow-lg), - 0 4px 8px var(--mageforge-shadow-md); - background-image: var(--gradient-brand-hover); -} - -.mageforge-toolbar-burger:active { - transform: translateY(0); -} - -.mageforge-toolbar-burger.mageforge-active { - background: var(--mageforge-color-green); -} - -.mageforge-toolbar-burger-logo { - display: flex; - align-items: center; - justify-content: center; - flex-shrink: 0; -} - -.mageforge-toolbar-burger-label { - font-family: var(--mageforge-font-family); - font-size: 14px; - font-weight: 600; - letter-spacing: 0.03em; - color: var(--mageforge-color-white); - white-space: nowrap; - line-height: 1; -} - -.mageforge-toolbar--no-labels .mageforge-toolbar-burger-label, -.mageforge-toolbar--no-labels .mageforge-inspector-float-button span { - display: none; -} - -/* ============================================================================ - Audit Menu - ========================================================================== */ - -/* - * Menu is hidden by default via display:none and animated open/close with - * @starting-style + transition-behavior:allow-discrete (Chrome 117+, Safari 17.5+). - * Older browsers get an instant show/hide without animation (graceful degradation). - */ -.mageforge-toolbar-menu { - position: absolute; - bottom: calc(100% + 8px); - left: 0; - background: linear-gradient( - 135deg, - var(--mageforge-bg-dark) 0%, - var(--mageforge-bg-dark-alt) 100% - ); - border: 1px solid var(--mageforge-border-color); - border-radius: 10px; - box-shadow: - 0 -8px 24px var(--mageforge-shadow-lg), - 0 6px 10px var(--mageforge-shadow-sm); - padding: 0 6px 6px; - min-width: 350px; - max-height: 90vh; - overflow-y: auto; - overflow-x: hidden; - font-family: var(--mageforge-font-family); - display: none; - opacity: 0; - transform: translateY(8px); - transition: - opacity 0.15s ease-out, - transform 0.15s ease-out, - display 0.15s allow-discrete; -} - -.mageforge-toolbar-menu.mageforge-menu-open { - display: block; - opacity: 1; - transform: translateY(0); -} - -@starting-style { - .mageforge-toolbar-menu.mageforge-menu-open { - opacity: 0; - transform: translateY(8px); - } -} - -.mageforge-toolbar-menu-title { - display: flex; - flex-wrap: wrap; - align-items: center; - justify-content: space-between; - padding: 10px 8px 2px; - border-bottom: 1px solid var(--mageforge-border-color); - margin-bottom: 4px; - position: sticky; - top: 0; - z-index: 99999; - background: linear-gradient( - 135deg, - var(--mageforge-bg-dark) 0%, - var(--mageforge-bg-dark-alt) 100% - ); -} - -.mageforge-toolbar-menu-title-text { - font-family: var(--mageforge-font-family); - font-size: 20px; - font-weight: 700; - letter-spacing: 0.08em; - padding-bottom: 8px; - color: transparent; - background-image: var(--gradient-brand); - background-clip: text; - -webkit-background-clip: text; - display: block; -} - -.mageforge-toolbar-menu-logo { - display: flex; - align-items: center; - padding-bottom: 8px; -} - -.mageforge-toolbar-menu-logo svg { - height: 24px; - width: auto; -} - -.mageforge-toolbar-menu-close { - background: none; - border: 1px solid var(--mageforge-border-color); - cursor: pointer; - color: var(--mageforge-color-slate-400); - padding: 6px; - line-height: 0; - border-radius: 6px; - transition: - color 0.15s ease, - background 0.15s ease; -} - -.mageforge-toolbar-menu-close:hover { - color: var(--mageforge-color-white); - background: var(--mageforge-surface-glass-hover); -} - -.mageforge-toolbar-menu-item { - display: flex; - align-items: center; - gap: 10px; - width: 100%; - padding: 8px 10px; - background: none; - border: 1px solid transparent; - border-radius: 7px; - cursor: pointer; - color: var(--mageforge-color-white); - text-align: left; - transition: - background 0.15s ease, - border-color 0.15s ease; - margin-bottom: 4px; -} - -.mageforge-toolbar-menu-item:hover { - background: var(--mageforge-surface-glass-hover); -} - -.mageforge-toolbar-menu-item.mageforge-active { - background: var(--mageforge-color-green-alpha-15); - border-color: var(--mageforge-color-green-alpha-35); -} - -.mageforge-toolbar-menu-item.mageforge-active .mageforge-toolbar-menu-label { - color: var(--mageforge-color-green); -} - -.mageforge-toolbar-menu-item.mageforge-active--error { - background: var(--mageforge-color-red-alpha-15); - border-color: var(--mageforge-color-red-alpha-35); -} - -.mageforge-toolbar-menu-item.mageforge-active--error - .mageforge-toolbar-menu-label { - color: var(--mageforge-color-red); -} - -.mageforge-toolbar-menu-item.mageforge-active--warning { - background: var(--mageforge-color-amber-alpha-15); - border-color: var(--mageforge-color-amber-alpha-35); -} - -.mageforge-toolbar-menu-item.mageforge-active--warning - .mageforge-toolbar-menu-label { - color: var(--mageforge-color-amber); -} - -.mageforge-toolbar-menu-icon { - font-size: 16px; - flex-shrink: 0; - width: 10%; - min-width: 32px; - display: flex; - align-items: center; - justify-content: center; -} - -.mageforge-toolbar-menu-text { - display: flex; - flex-direction: column; - gap: 1px; - flex: 1; -} - -.mageforge-toolbar-menu-toggle { - flex-shrink: 0; - width: 28px; - height: 16px; - border-radius: 8px; - background: var(--mageforge-toggle-bg-off); - position: relative; - transition: background 0.2s ease; - margin-left: auto; -} - -.mageforge-toolbar-menu-toggle::after { - content: ""; - position: absolute; - width: 12px; - height: 12px; - border-radius: 50%; - background: var(--mageforge-color-white); - top: 2px; - left: 2px; - transition: transform 0.2s ease; -} - -.mageforge-toolbar-menu-item.mageforge-active .mageforge-toolbar-menu-toggle { - background: var(--mageforge-color-green); -} - -.mageforge-toolbar-menu-item.mageforge-active - .mageforge-toolbar-menu-toggle::after { - transform: translateX(12px); -} - -.mageforge-toolbar-menu-label { - font-size: 12px; - font-weight: 600; - color: var(--mageforge-color-white); -} - -.mageforge-toolbar-menu-desc { - color: var(--mageforge-color-slate-400); - font-size: 11px; - line-height: 1.3; - user-select: text; - cursor: default; -} - -.mageforge-toolbar-menu-desc.mageforge-active { - color: var(--mageforge-color-orange); - font-size: 12px; - user-select: text; -} - -.mageforge-toolbar-menu-label-row { - display: flex; - align-items: center; - gap: 6px; -} - -.mageforge-toolbar-menu-status { - font-size: 9px; - font-weight: 700; - line-height: 1; - padding: 2px 6px; - border-radius: 10px; - white-space: nowrap; - opacity: 0; - transform: scale(0.8); - transition: - opacity 0.2s ease, - transform 0.2s ease; -} - -.mageforge-toolbar-menu-status:not(:empty) { - opacity: 1; - transform: scale(1); -} - -.mageforge-toolbar-menu-status--success { - color: var(--mageforge-color-green); - background: var(--mageforge-color-green-alpha-15); - border: 1px solid var(--mageforge-color-green-alpha-35); -} - -.mageforge-toolbar-menu-status--error { - color: var(--mageforge-color-red); - background: var(--mageforge-color-red-alpha-15); - border: 1px solid var(--mageforge-color-red-alpha-35); -} - -.mageforge-toolbar-menu-status--warning { - color: var(--mageforge-color-amber); - background: var(--mageforge-color-amber-alpha-15); - border: 1px solid var(--mageforge-color-amber-alpha-35); -} - -/* ============================================================================ - Menu Groups - ========================================================================== */ - -.mageforge-toolbar-menu-group { - margin-bottom: 4px; -} - -.mageforge-toolbar-menu-group-header { - display: flex; - align-items: center; - justify-content: space-between; - width: 100%; - padding: 4px 4px 4px 6px; - background: none; - border: none; - border-bottom: 1px solid var(--mageforge-border-color); - cursor: pointer; - margin-bottom: 4px; -} - -.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); -} - -.mageforge-toolbar-menu-group-label { - font-family: var(--mageforge-font-family); - font-size: 11px; - font-weight: 600; - text-transform: uppercase; - transition: color 0.15s ease; - letter-spacing: 0.08em; - 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 { - color: var(--mageforge-color-slate-400); - line-height: 0; - transition: - color 0.15s ease, - transform 0.2s ease; -} - -.mageforge-toolbar-menu-group--collapsed .mageforge-toolbar-menu-group-chevron { - transform: rotate(-90deg); -} - -.mageforge-toolbar-menu-group-items { - overflow: hidden; -} - -.mageforge-toolbar-menu-group--collapsed .mageforge-toolbar-menu-group-items { - display: none; -} - -/* ============================================================================ - Audit Highlights - ========================================================================== */ - -/* Fixed-position overlay injected over every highlighted element */ -.mageforge-audit-overlay { - position: fixed; - pointer-events: none; - background-color: var(--mageforge-color-red-alpha-35); - outline: 3px solid var(--mageforge-color-red); - outline-offset: 0; - z-index: 9999997; -} - -.mageforge-audit-overlay--warning { - background-color: var(--mageforge-color-amber-alpha-35); - outline-color: var(--mageforge-color-amber); - outline-style: dashed; -} - -/* ============================================================================ - Feedback Toast - ========================================================================== */ - -.mageforge-toolbar-feedback { - position: fixed; - font-family: var(--mageforge-font-family); - font-size: 12px; - font-weight: 600; - padding: 8px 14px; - border-radius: 10px; - backdrop-filter: blur(8px); - white-space: nowrap; - pointer-events: none; - z-index: 9999999; - animation: - mageforge-toolbar-slide-up 0.2s ease-out, - mageforge-toolbar-fade-out 0.4s ease-in 2.6s forwards; -} - -.mageforge-toolbar-feedback--success { - color: var(--mageforge-color-green); - background: var(--mageforge-color-green-alpha-15); - border: 1px solid var(--mageforge-color-green-alpha-35); - box-shadow: 0 4px 12px var(--mageforge-color-green-alpha-15); -} - -.mageforge-toolbar-feedback--error { - color: var(--mageforge-color-red); - background: var(--mageforge-color-red-alpha-15); - border: 1px solid var(--mageforge-color-red-alpha-35); - box-shadow: 0 4px 12px var(--mageforge-color-red-alpha-15); -} - -/* ============================================================================ - Animations - ========================================================================== */ - -/* mageforge-toolbar-slide-up is still used by the feedback toast */ -@keyframes mageforge-toolbar-slide-up { - from { - opacity: 0; - transform: translateY(8px); - } - to { - opacity: 1; - transform: translateY(0); - } -} - -@keyframes mageforge-toolbar-fade-out { - from { - opacity: 1; - } - to { - opacity: 0; - } -} - -/* ============================================================================ - Menu Footer - ========================================================================== */ - -.mageforge-toolbar-menu-footer { - border-top: 1px solid var(--mageforge-border-color); - margin-top: 4px; - padding-top: 6px; -} - -/* ============================================================================ - Health Score Gauge - ========================================================================== */ - -.mageforge-toolbar-health-wrapper { - display: flex; - flex-direction: column; - align-items: center; - padding: 4px 12px 6px; - gap: 2px; -} - -.mageforge-toolbar-health-gauge { - width: 130px; - height: 75px; - overflow: visible; -} - -.mageforge-health-gauge-progress { - transition: stroke-dasharray 0.7s cubic-bezier(0.4, 0, 0.2, 1); -} - -.mageforge-health-gauge-needle { - transition: - x2 0.7s cubic-bezier(0.4, 0, 0.2, 1), - y2 0.7s cubic-bezier(0.4, 0, 0.2, 1), - opacity 0.3s ease; -} - -.mageforge-toolbar-health-score-text { - text-align: center; -} - -.mageforge-toolbar-health-score-value { - font-family: var(--mageforge-font-family); - font-size: 18px; - font-weight: 700; - color: var(--mageforge-color-white); - line-height: 1.2; -} - -.mageforge-toolbar-health-score-max { - font-size: 12px; - font-weight: 400; - color: var(--mageforge-color-slate-400); -} - -.mageforge-toolbar-health-score-label { - font-family: var(--mageforge-font-family); - font-size: 10px; - font-weight: 500; - letter-spacing: 0.05em; - text-transform: uppercase; - color: var(--mageforge-color-slate-400); - margin-top: 2px; -} - -/* ============================================================================ - Run All Tests Button Row - ========================================================================== */ - -.mageforge-toolbar-menu-button-row { - display: flex; - align-items: stretch; - gap: 6px; - margin-bottom: 4px; -} - -.mageforge-toolbar-menu-run-all { - display: flex; - align-items: center; - justify-content: center; - gap: 8px; - flex: 1; - padding: 9px 16px; - background: linear-gradient( - 90deg, - var(--mageforge-color-blue) 0%, - var(--mageforge-color-pink) 100% - ); - border: none; - border-radius: 7px; - cursor: pointer; - color: var(--mageforge-color-white); - font-family: var(--mageforge-font-family); - font-size: 12px; - font-weight: 700; - letter-spacing: 0.08em; - text-transform: uppercase; - transition: - opacity 0.15s ease, - transform 0.15s ease, - box-shadow 0.15s ease; - box-shadow: 0 4px 14px rgba(59, 130, 246, 0.3); -} - -.mageforge-toolbar-menu-run-all:hover { - opacity: 0.9; - transform: translateY(-1px); - box-shadow: 0 6px 18px rgba(59, 130, 246, 0.4); -} - -.mageforge-toolbar-menu-run-all:active { - transform: translateY(0); -} - -.mageforge-toolbar-menu-run-all:disabled, -.mageforge-toolbar-menu-run-all.mageforge-running { - opacity: 0.6; - cursor: not-allowed; - transform: none; -} - -.mageforge-toolbar-menu-reset { - display: flex; - align-items: center; - justify-content: center; - padding: 9px 10px; - background: none; - border: 1px solid var(--mageforge-border-color); - border-radius: 7px; - cursor: pointer; - color: var(--mageforge-color-slate-400); - line-height: 0; - transition: - color 0.15s ease, - background 0.15s ease, - border-color 0.15s ease; -} - -.mageforge-toolbar-menu-reset:hover { - color: var(--mageforge-color-white); - background: var(--mageforge-surface-glass-hover); - border-color: var(--mageforge-border-glass); -} - -/* ============================================================================ - Menu Credit - ========================================================================== */ - -.mageforge-toolbar-menu-credit { - margin-top: 8px; - text-align: center; - font-family: var(--mageforge-font-family); - font-size: 10px; - color: var(--mageforge-color-slate-400); -} - -.mageforge-toolbar-menu-credit-heart { - color: var(--mageforge-color-red); -} - -.mageforge-toolbar-menu-credit-link { - color: var(--mageforge-color-slate-400); - text-decoration: none; - transition: color 0.15s ease; -} - -.mageforge-toolbar-menu-credit-link:hover { - color: var(--mageforge-color-blue); -} - -/* ============================================================================ - Position Variants - ========================================================================== */ - -.mageforge-toolbar[data-position="bottom-right"] { - bottom: 16px; - left: auto; - right: 16px; -} - -.mageforge-toolbar[data-position="top-left"] { - bottom: auto; - top: 16px; - left: 16px; -} - -.mageforge-toolbar[data-position="top-right"] { - bottom: auto; - top: 16px; - left: auto; - right: 16px; -} - -/* Menu opens downward for top positions */ -.mageforge-toolbar[data-position^="top"] .mageforge-toolbar-menu { - bottom: auto; - top: calc(100% + 8px); - transform: translateY(-8px); -} - -.mageforge-toolbar[data-position^="top"] - .mageforge-toolbar-menu.mageforge-menu-open { - transform: translateY(0); - - @starting-style { - transform: translateY(-8px); - } -} - -/* Menu anchors to right edge for right positions */ -.mageforge-toolbar[data-position$="right"] .mageforge-toolbar-menu { - left: auto; - right: 0; -} - -/* ============================================================================ - Theme Overrides - ========================================================================== */ - -/* - * Only the menu popup switches to light; the burger button intentionally stays - * dark so it remains visible regardless of the page background colour. - */ - -/* Light Mode */ -.mageforge-toolbar[data-theme="light"] .mageforge-toolbar-menu { - --mageforge-bg-dark: rgba(255, 255, 255, 0.98); - --mageforge-bg-dark-alt: rgba(248, 250, 252, 0.98); - --mageforge-border-color: rgba(0, 0, 0, 0.12); - --mageforge-surface-glass-hover: rgba(0, 0, 0, 0.05); - --mageforge-shadow-sm: rgba(0, 0, 0, 0.1); - --mageforge-shadow-md: rgba(0, 0, 0, 0.15); - --mageforge-shadow-lg: rgba(0, 0, 0, 0.2); - --mageforge-color-white: #0f172a; - --mageforge-color-slate-400: #64748b; -} - -.mageforge-toolbar[data-theme="light"] .mageforge-toolbar-menu-toggle { - background: var(--mageforge-toggle-bg-off-light); -} - -/* - * Auto Mode – Light System Preference - * NOTE: Keep these custom-property overrides in sync with the [data-theme="light"] block above. - */ -@media (prefers-color-scheme: light) { - .mageforge-toolbar[data-theme="auto"] .mageforge-toolbar-menu { - --mageforge-bg-dark: rgba(255, 255, 255, 0.98); - --mageforge-bg-dark-alt: rgba(248, 250, 252, 0.98); - --mageforge-border-color: rgba(0, 0, 0, 0.12); - --mageforge-surface-glass-hover: rgba(0, 0, 0, 0.05); - --mageforge-shadow-sm: rgba(0, 0, 0, 0.1); - --mageforge-shadow-md: rgba(0, 0, 0, 0.15); - --mageforge-shadow-lg: rgba(0, 0, 0, 0.2); - --mageforge-color-white: #0f172a; - --mageforge-color-slate-400: #64748b; - } - - .mageforge-toolbar[data-theme="auto"] .mageforge-toolbar-menu-toggle { - background: var(--mageforge-toggle-bg-off-light); - } -} - -/* ============================================================================ - Responsive - ========================================================================== */ - -@media (max-width: 640px) { - .mageforge-toolbar { - bottom: 10px; - left: 10px; - } - .mageforge-toolbar[data-position="bottom-right"] { - left: auto; - right: 10px; - } - .mageforge-toolbar[data-position="top-left"] { - bottom: auto; - left: 10px; - top: 10px; - } - .mageforge-toolbar[data-position="top-right"] { - bottom: auto; - left: auto; - right: 10px; - top: 10px; - } - .mageforge-toolbar-menu { - min-width: 300px; - max-height: calc(100vh - 60px); - overflow: auto; - } - .mageforge-toolbar-burger-label { - display: none; - } - .mageforge-toolbar-burger { - height: 32px; - width: auto; - } -} +@import url('toolbar/_variables.css'); +@import url('toolbar/_reset.css'); +@import url('toolbar/_burger.css'); +@import url('toolbar/_menu.css'); +@import url('toolbar/_groups.css'); +@import url('toolbar/_highlights.css'); +@import url('toolbar/_feedback.css'); +@import url('toolbar/_animations.css'); +@import url('toolbar/_footer.css'); +@import url('toolbar/_health.css'); +@import url('toolbar/_buttons.css'); +@import url('toolbar/_credit.css'); +@import url('toolbar/_positions.css'); +@import url('toolbar/_themes.css'); +@import url('toolbar/_responsive.css'); diff --git a/src/view/frontend/web/css/toolbar/_animations.css b/src/view/frontend/web/css/toolbar/_animations.css new file mode 100644 index 0000000..47a2be5 --- /dev/null +++ b/src/view/frontend/web/css/toolbar/_animations.css @@ -0,0 +1,28 @@ +/** + * MageForge Toolbar - Animations + * + * Keyframe animations used across the toolbar component. + * + * @package OpenForgeProject\MageForge + * @license GPL-3.0 + */ + +@keyframes mageforge-toolbar-slide-up { + from { + opacity: 0; + transform: translateY(8px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes mageforge-toolbar-fade-out { + from { + opacity: 1; + } + to { + opacity: 0; + } +} diff --git a/src/view/frontend/web/css/toolbar/_burger.css b/src/view/frontend/web/css/toolbar/_burger.css new file mode 100644 index 0000000..707e75f --- /dev/null +++ b/src/view/frontend/web/css/toolbar/_burger.css @@ -0,0 +1,86 @@ +/** + * MageForge Toolbar - Burger Button + * + * Styles for the main toolbar toggle button. + * + * @package OpenForgeProject\MageForge + * @license GPL-3.0 + */ + +/* ============================================================================ + Toolbar Container + ========================================================================== */ + +.mageforge-toolbar { + position: fixed; + bottom: 16px; + left: 16px; + display: flex; + align-items: center; + gap: 8px; + z-index: 9999998; + pointer-events: auto; +} + +/* ============================================================================ + Burger Button + ========================================================================== */ + +.mageforge-toolbar-burger { + height: 36px; + padding: 0 10px 0 8px; + background: var(--mageforge-burger-bg); + color: var(--mageforge-color-white); + border-radius: 5px; + cursor: pointer; + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + gap: 8px; + transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); + box-shadow: + 0 4px 12px var(--mageforge-shadow-md), + 0 2px 4px var(--mageforge-shadow-sm); + flex-shrink: 0; + background-image: var(--gradient-brand); +} + +.mageforge-toolbar-burger:hover { + background: var(--mageforge-burger-bg-hover); + transform: translateY(-2px); + box-shadow: + 0 8px 20px var(--mageforge-shadow-lg), + 0 4px 8px var(--mageforge-shadow-md); + background-image: var(--gradient-brand-hover); +} + +.mageforge-toolbar-burger:active { + transform: translateY(0); +} + +.mageforge-toolbar-burger.mageforge-active { + background: var(--mageforge-color-green); +} + +.mageforge-toolbar-burger-logo { + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} + +.mageforge-toolbar-burger-label { + font-family: var(--mageforge-font-family); + font-size: 14px; + font-weight: 600; + letter-spacing: 0.03em; + color: var(--mageforge-color-white); + white-space: nowrap; + line-height: 1; +} + +.mageforge-toolbar--no-labels .mageforge-toolbar-burger-label, +.mageforge-toolbar--no-labels .mageforge-inspector-float-button span { + display: none; +} diff --git a/src/view/frontend/web/css/toolbar/_buttons.css b/src/view/frontend/web/css/toolbar/_buttons.css new file mode 100644 index 0000000..898b3bf --- /dev/null +++ b/src/view/frontend/web/css/toolbar/_buttons.css @@ -0,0 +1,83 @@ +/** + * MageForge Toolbar - Run All Tests Button Row + * + * Styles for the "Run All" and "Reset" buttons in the menu footer. + * + * @package OpenForgeProject\MageForge + * @license GPL-3.0 + */ + +.mageforge-toolbar-menu-button-row { + display: flex; + align-items: stretch; + gap: 6px; + margin-bottom: 4px; +} + +.mageforge-toolbar-menu-run-all { + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + flex: 1; + padding: 9px 16px; + background: linear-gradient( + 90deg, + var(--mageforge-color-blue) 0%, + var(--mageforge-color-pink) 100% + ); + border: none; + border-radius: 7px; + cursor: pointer; + color: var(--mageforge-color-white); + font-family: var(--mageforge-font-family); + font-size: 12px; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; + transition: + opacity 0.15s ease, + transform 0.15s ease, + box-shadow 0.15s ease; + box-shadow: 0 4px 14px rgba(59, 130, 246, 0.3); +} + +.mageforge-toolbar-menu-run-all:hover { + opacity: 0.9; + transform: translateY(-1px); + box-shadow: 0 6px 18px rgba(59, 130, 246, 0.4); +} + +.mageforge-toolbar-menu-run-all:active { + transform: translateY(0); +} + +.mageforge-toolbar-menu-run-all:disabled, +.mageforge-toolbar-menu-run-all.mageforge-running { + opacity: 0.6; + cursor: not-allowed; + transform: none; +} + +.mageforge-toolbar-menu-reset { + display: flex; + align-items: center; + justify-content: center; + padding: 9px 10px; + background: none; + border: 1px solid var(--mageforge-border-color); + border-radius: 7px; + cursor: pointer; + color: var(--mageforge-color-slate-400); + line-height: 0; + transition: + color 0.15s ease, + background 0.15s ease, + border-color 0.15s ease; +} + +.mageforge-toolbar-menu-reset:hover { + color: var(--mageforge-color-white); + background: var(--mageforge-surface-glass-hover); + border-color: var(--mageforge-border-glass); +} diff --git a/src/view/frontend/web/css/toolbar/_credit.css b/src/view/frontend/web/css/toolbar/_credit.css new file mode 100644 index 0000000..fad64dc --- /dev/null +++ b/src/view/frontend/web/css/toolbar/_credit.css @@ -0,0 +1,30 @@ +/** + * MageForge Toolbar - Menu Credit + * + * Styles for the "Made with ❤️" credit link at the bottom of the menu. + * + * @package OpenForgeProject\MageForge + * @license GPL-3.0 + */ + +.mageforge-toolbar-menu-credit { + margin-top: 8px; + text-align: center; + font-family: var(--mageforge-font-family); + font-size: 10px; + color: var(--mageforge-color-slate-400); +} + +.mageforge-toolbar-menu-credit-heart { + color: var(--mageforge-color-red); +} + +.mageforge-toolbar-menu-credit-link { + color: var(--mageforge-color-slate-400); + text-decoration: none; + transition: color 0.15s ease; +} + +.mageforge-toolbar-menu-credit-link:hover { + color: var(--mageforge-color-blue); +} diff --git a/src/view/frontend/web/css/toolbar/_feedback.css b/src/view/frontend/web/css/toolbar/_feedback.css new file mode 100644 index 0000000..d73f6c0 --- /dev/null +++ b/src/view/frontend/web/css/toolbar/_feedback.css @@ -0,0 +1,38 @@ +/** + * MageForge Toolbar - Feedback Toast + * + * Styles for success/error feedback toasts. + * + * @package OpenForgeProject\MageForge + * @license GPL-3.0 + */ + +.mageforge-toolbar-feedback { + position: fixed; + font-family: var(--mageforge-font-family); + font-size: 12px; + font-weight: 600; + padding: 8px 14px; + border-radius: 10px; + backdrop-filter: blur(8px); + white-space: nowrap; + pointer-events: none; + z-index: 9999999; + animation: + mageforge-toolbar-slide-up 0.2s ease-out, + mageforge-toolbar-fade-out 0.4s ease-in 2.6s forwards; +} + +.mageforge-toolbar-feedback--success { + color: var(--mageforge-color-green); + background: var(--mageforge-color-green-alpha-15); + border: 1px solid var(--mageforge-color-green-alpha-35); + box-shadow: 0 4px 12px var(--mageforge-color-green-alpha-15); +} + +.mageforge-toolbar-feedback--error { + color: var(--mageforge-color-red); + background: var(--mageforge-color-red-alpha-15); + border: 1px solid var(--mageforge-color-red-alpha-35); + box-shadow: 0 4px 12px var(--mageforge-color-red-alpha-15); +} diff --git a/src/view/frontend/web/css/toolbar/_footer.css b/src/view/frontend/web/css/toolbar/_footer.css new file mode 100644 index 0000000..10d97da --- /dev/null +++ b/src/view/frontend/web/css/toolbar/_footer.css @@ -0,0 +1,14 @@ +/** + * MageForge Toolbar - Menu Footer + * + * Styles for the menu footer section. + * + * @package OpenForgeProject\MageForge + * @license GPL-3.0 + */ + +.mageforge-toolbar-menu-footer { + border-top: 1px solid var(--mageforge-border-color); + margin-top: 4px; + padding-top: 6px; +} diff --git a/src/view/frontend/web/css/toolbar/_groups.css b/src/view/frontend/web/css/toolbar/_groups.css new file mode 100644 index 0000000..4e417a9 --- /dev/null +++ b/src/view/frontend/web/css/toolbar/_groups.css @@ -0,0 +1,93 @@ +/** + * MageForge Toolbar - Menu Groups + * + * Styles for collapsible audit groups with headers, chevrons and group labels. + * + * @package OpenForgeProject\MageForge + * @license GPL-3.0 + */ + +.mageforge-toolbar-menu-group { + margin-bottom: 4px; +} + +.mageforge-toolbar-menu-group-header { + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + padding: 4px 4px 4px 6px; + background: none; + border: none; + border-bottom: 1px solid var(--mageforge-border-color); + cursor: pointer; + margin-bottom: 4px; +} + +.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); +} + +.mageforge-toolbar-menu-group-label { + font-family: var(--mageforge-font-family); + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + transition: color 0.15s ease; + letter-spacing: 0.08em; + 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 { + color: var(--mageforge-color-slate-400); + line-height: 0; + transition: + color 0.15s ease, + transform 0.2s ease; +} + +.mageforge-toolbar-menu-group--collapsed .mageforge-toolbar-menu-group-chevron { + transform: rotate(-90deg); +} + +.mageforge-toolbar-menu-group-items { + overflow: hidden; +} + +.mageforge-toolbar-menu-group--collapsed .mageforge-toolbar-menu-group-items { + display: none; +} diff --git a/src/view/frontend/web/css/toolbar/_health.css b/src/view/frontend/web/css/toolbar/_health.css new file mode 100644 index 0000000..7ebfc27 --- /dev/null +++ b/src/view/frontend/web/css/toolbar/_health.css @@ -0,0 +1,61 @@ +/** + * MageForge Toolbar - Health Score Gauge + * + * SVG-based health score visualization. + * + * @package OpenForgeProject\MageForge + * @license GPL-3.0 + */ + +.mageforge-toolbar-health-wrapper { + display: flex; + flex-direction: column; + align-items: center; + padding: 4px 12px 6px; + gap: 2px; +} + +.mageforge-toolbar-health-gauge { + width: 130px; + height: 75px; + overflow: visible; +} + +.mageforge-health-gauge-progress { + transition: stroke-dasharray 0.7s cubic-bezier(0.4, 0, 0.2, 1); +} + +.mageforge-health-gauge-needle { + transition: + x2 0.7s cubic-bezier(0.4, 0, 0.2, 1), + y2 0.7s cubic-bezier(0.4, 0, 0.2, 1), + opacity 0.3s ease; +} + +.mageforge-toolbar-health-score-text { + text-align: center; +} + +.mageforge-toolbar-health-score-value { + font-family: var(--mageforge-font-family); + font-size: 18px; + font-weight: 700; + color: var(--mageforge-color-white); + line-height: 1.2; +} + +.mageforge-toolbar-health-score-max { + font-size: 12px; + font-weight: 400; + color: var(--mageforge-color-slate-400); +} + +.mageforge-toolbar-health-score-label { + font-family: var(--mageforge-font-family); + font-size: 10px; + font-weight: 500; + letter-spacing: 0.05em; + text-transform: uppercase; + color: var(--mageforge-color-slate-400); + margin-top: 2px; +} diff --git a/src/view/frontend/web/css/toolbar/_highlights.css b/src/view/frontend/web/css/toolbar/_highlights.css new file mode 100644 index 0000000..96b581c --- /dev/null +++ b/src/view/frontend/web/css/toolbar/_highlights.css @@ -0,0 +1,23 @@ +/** + * MageForge Toolbar - Audit Highlights + * + * Fixed-position overlay injected over every highlighted element. + * + * @package OpenForgeProject\MageForge + * @license GPL-3.0 + */ + +.mageforge-audit-overlay { + position: fixed; + pointer-events: none; + background-color: var(--mageforge-color-red-alpha-35); + outline: 3px solid var(--mageforge-color-red); + outline-offset: 0; + z-index: 9999997; +} + +.mageforge-audit-overlay--warning { + background-color: var(--mageforge-color-amber-alpha-35); + outline-color: var(--mageforge-color-amber); + outline-style: dashed; +} diff --git a/src/view/frontend/web/css/toolbar/_menu.css b/src/view/frontend/web/css/toolbar/_menu.css new file mode 100644 index 0000000..4fd49ad --- /dev/null +++ b/src/view/frontend/web/css/toolbar/_menu.css @@ -0,0 +1,307 @@ +/** + * MageForge Toolbar - Audit Menu + * + * Styles for the menu popup: container, title bar, items, toggles, labels, + * descriptions and status badges. + * + * @package OpenForgeProject\MageForge + * @license GPL-3.0 + */ + +/* ============================================================================ + Audit Menu + ========================================================================== */ + +/* + * Menu is hidden by default via display:none and animated open/close with + * @starting-style + transition-behavior:allow-discrete (Chrome 117+, Safari 17.5+). + * Older browsers get an instant show/hide without animation (graceful degradation). + */ +.mageforge-toolbar-menu { + position: absolute; + bottom: calc(100% + 8px); + left: 0; + background: linear-gradient( + 135deg, + var(--mageforge-bg-dark) 0%, + var(--mageforge-bg-dark-alt) 100% + ); + border: 1px solid var(--mageforge-border-color); + border-radius: 10px; + box-shadow: + 0 -8px 24px var(--mageforge-shadow-lg), + 0 6px 10px var(--mageforge-shadow-sm); + padding: 0 6px 6px; + min-width: 350px; + max-height: 90vh; + overflow-y: auto; + overflow-x: hidden; + font-family: var(--mageforge-font-family); + display: none; + opacity: 0; + transform: translateY(8px); + transition: + opacity 0.15s ease-out, + transform 0.15s ease-out, + display 0.15s allow-discrete; +} + +.mageforge-toolbar-menu.mageforge-menu-open { + display: block; + opacity: 1; + transform: translateY(0); +} + +@starting-style { + .mageforge-toolbar-menu.mageforge-menu-open { + opacity: 0; + transform: translateY(8px); + } +} + +/* ============================================================================ + Menu Title Bar + ========================================================================== */ + +.mageforge-toolbar-menu-title { + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: space-between; + padding: 10px 8px 2px; + border-bottom: 1px solid var(--mageforge-border-color); + margin-bottom: 4px; + position: sticky; + top: 0; + z-index: 99999; + background: linear-gradient( + 135deg, + var(--mageforge-bg-dark) 0%, + var(--mageforge-bg-dark-alt) 100% + ); +} + +.mageforge-toolbar-menu-title-text { + font-family: var(--mageforge-font-family); + font-size: 20px; + font-weight: 700; + letter-spacing: 0.08em; + padding-bottom: 8px; + color: transparent; + background-image: var(--gradient-brand); + background-clip: text; + -webkit-background-clip: text; + display: block; +} + +.mageforge-toolbar-menu-logo { + display: flex; + align-items: center; + padding-bottom: 8px; +} + +.mageforge-toolbar-menu-logo svg { + height: 24px; + width: auto; +} + +.mageforge-toolbar-menu-close { + background: none; + border: 1px solid var(--mageforge-border-color); + cursor: pointer; + color: var(--mageforge-color-slate-400); + padding: 6px; + line-height: 0; + border-radius: 6px; + transition: + color 0.15s ease, + background 0.15s ease; +} + +.mageforge-toolbar-menu-close:hover { + color: var(--mageforge-color-white); + background: var(--mageforge-surface-glass-hover); +} + +/* ============================================================================ + Menu Items + ========================================================================== */ + +.mageforge-toolbar-menu-item { + display: flex; + align-items: center; + gap: 10px; + width: 100%; + padding: 8px 10px; + background: none; + border: 1px solid transparent; + border-radius: 7px; + cursor: pointer; + color: var(--mageforge-color-white); + text-align: left; + transition: + background 0.15s ease, + border-color 0.15s ease; + margin-bottom: 4px; +} + +.mageforge-toolbar-menu-item:hover { + background: var(--mageforge-surface-glass-hover); +} + +.mageforge-toolbar-menu-item.mageforge-active { + background: var(--mageforge-color-green-alpha-15); + border-color: var(--mageforge-color-green-alpha-35); +} + +.mageforge-toolbar-menu-item.mageforge-active .mageforge-toolbar-menu-label { + color: var(--mageforge-color-green); +} + +.mageforge-toolbar-menu-item.mageforge-active--error { + background: var(--mageforge-color-red-alpha-15); + border-color: var(--mageforge-color-red-alpha-35); +} + +.mageforge-toolbar-menu-item.mageforge-active--error + .mageforge-toolbar-menu-label { + color: var(--mageforge-color-red); +} + +.mageforge-toolbar-menu-item.mageforge-active--warning { + background: var(--mageforge-color-amber-alpha-15); + border-color: var(--mageforge-color-amber-alpha-35); +} + +.mageforge-toolbar-menu-item.mageforge-active--warning + .mageforge-toolbar-menu-label { + color: var(--mageforge-color-amber); +} + +/* ============================================================================ + Menu Item Content + ========================================================================== */ + +.mageforge-toolbar-menu-icon { + font-size: 16px; + flex-shrink: 0; + width: 10%; + min-width: 32px; + display: flex; + align-items: center; + justify-content: center; +} + +.mageforge-toolbar-menu-text { + display: flex; + flex-direction: column; + gap: 1px; + flex: 1; +} + +/* ============================================================================ + Toggle Switch + ========================================================================== */ + +.mageforge-toolbar-menu-toggle { + flex-shrink: 0; + width: 28px; + height: 16px; + border-radius: 8px; + background: var(--mageforge-toggle-bg-off); + position: relative; + transition: background 0.2s ease; + margin-left: auto; +} + +.mageforge-toolbar-menu-toggle::after { + content: ""; + position: absolute; + width: 12px; + height: 12px; + border-radius: 50%; + background: var(--mageforge-color-white); + top: 2px; + left: 2px; + transition: transform 0.2s ease; +} + +.mageforge-toolbar-menu-item.mageforge-active .mageforge-toolbar-menu-toggle { + background: var(--mageforge-color-green); +} + +.mageforge-toolbar-menu-item.mageforge-active + .mageforge-toolbar-menu-toggle::after { + transform: translateX(12px); +} + +/* ============================================================================ + Labels & Descriptions + ========================================================================== */ + +.mageforge-toolbar-menu-label { + font-size: 12px; + font-weight: 600; + color: var(--mageforge-color-white); +} + +.mageforge-toolbar-menu-desc { + color: var(--mageforge-color-slate-400); + font-size: 11px; + line-height: 1.3; + user-select: text; + cursor: default; +} + +.mageforge-toolbar-menu-desc.mageforge-active { + color: var(--mageforge-color-orange); + font-size: 12px; + user-select: text; +} + +.mageforge-toolbar-menu-label-row { + display: flex; + align-items: center; + gap: 6px; +} + +/* ============================================================================ + Status Badges + ========================================================================== */ + +.mageforge-toolbar-menu-status { + font-size: 9px; + font-weight: 700; + line-height: 1; + padding: 2px 6px; + border-radius: 10px; + white-space: nowrap; + opacity: 0; + transform: scale(0.8); + transition: + opacity 0.2s ease, + transform 0.2s ease; +} + +.mageforge-toolbar-menu-status:not(:empty) { + opacity: 1; + transform: scale(1); +} + +.mageforge-toolbar-menu-status--success { + color: var(--mageforge-color-green); + background: var(--mageforge-color-green-alpha-15); + border: 1px solid var(--mageforge-color-green-alpha-35); +} + +.mageforge-toolbar-menu-status--error { + color: var(--mageforge-color-red); + background: var(--mageforge-color-red-alpha-15); + border: 1px solid var(--mageforge-color-red-alpha-35); +} + +.mageforge-toolbar-menu-status--warning { + color: var(--mageforge-color-amber); + background: var(--mageforge-color-amber-alpha-15); + border: 1px solid var(--mageforge-color-amber-alpha-35); +} diff --git a/src/view/frontend/web/css/toolbar/_positions.css b/src/view/frontend/web/css/toolbar/_positions.css new file mode 100644 index 0000000..70b7acf --- /dev/null +++ b/src/view/frontend/web/css/toolbar/_positions.css @@ -0,0 +1,49 @@ +/** + * MageForge Toolbar - Position Variants + * + * Styles for different toolbar positions on the page. + * + * @package OpenForgeProject\MageForge + * @license GPL-3.0 + */ + +.mageforge-toolbar[data-position="bottom-right"] { + bottom: 16px; + left: auto; + right: 16px; +} + +.mageforge-toolbar[data-position="top-left"] { + bottom: auto; + top: 16px; + left: 16px; +} + +.mageforge-toolbar[data-position="top-right"] { + bottom: auto; + top: 16px; + left: auto; + right: 16px; +} + +/* Menu opens downward for top positions */ +.mageforge-toolbar[data-position^="top"] .mageforge-toolbar-menu { + bottom: auto; + top: calc(100% + 8px); + transform: translateY(-8px); +} + +.mageforge-toolbar[data-position^="top"] + .mageforge-toolbar-menu.mageforge-menu-open { + transform: translateY(0); + + @starting-style { + transform: translateY(-8px); + } +} + +/* Menu anchors to right edge for right positions */ +.mageforge-toolbar[data-position$="right"] .mageforge-toolbar-menu { + left: auto; + right: 0; +} diff --git a/src/view/frontend/web/css/toolbar/_reset.css b/src/view/frontend/web/css/toolbar/_reset.css new file mode 100644 index 0000000..0fcc7ff --- /dev/null +++ b/src/view/frontend/web/css/toolbar/_reset.css @@ -0,0 +1,16 @@ +/** + * MageForge Toolbar - Reset & Base + * + * Box-sizing, margin/padding resets for namespace isolation. + * + * @package OpenForgeProject\MageForge + * @license GPL-3.0 + */ + +.mageforge-toolbar *, +.mageforge-toolbar *::before, +.mageforge-toolbar *::after { + box-sizing: border-box; + margin: 0; + padding: 0; +} diff --git a/src/view/frontend/web/css/toolbar/_responsive.css b/src/view/frontend/web/css/toolbar/_responsive.css new file mode 100644 index 0000000..8333b66 --- /dev/null +++ b/src/view/frontend/web/css/toolbar/_responsive.css @@ -0,0 +1,42 @@ +/** + * MageForge Toolbar - Responsive + * + * Mobile-first breakpoints for toolbar and menu sizing. + * + * @package OpenForgeProject\MageForge + * @license GPL-3.0 + */ + +@media (max-width: 640px) { + .mageforge-toolbar { + bottom: 10px; + left: 10px; + } + .mageforge-toolbar[data-position="bottom-right"] { + left: auto; + right: 10px; + } + .mageforge-toolbar[data-position="top-left"] { + bottom: auto; + left: 10px; + top: 10px; + } + .mageforge-toolbar[data-position="top-right"] { + bottom: auto; + left: auto; + right: 10px; + top: 10px; + } + .mageforge-toolbar-menu { + min-width: 300px; + max-height: calc(100vh - 60px); + overflow: auto; + } + .mageforge-toolbar-burger-label { + display: none; + } + .mageforge-toolbar-burger { + height: 32px; + width: auto; + } +} diff --git a/src/view/frontend/web/css/toolbar/_themes.css b/src/view/frontend/web/css/toolbar/_themes.css new file mode 100644 index 0000000..206c97b --- /dev/null +++ b/src/view/frontend/web/css/toolbar/_themes.css @@ -0,0 +1,52 @@ +/** + * MageForge Toolbar - Theme Overrides + * + * Light mode and auto mode color scheme overrides for the menu popup. + * The burger button intentionally stays dark for visibility. + * + * @package OpenForgeProject\MageForge + * @license GPL-3.0 + */ + +/* ============================================================================ + Light Mode + ========================================================================== */ + +.mageforge-toolbar[data-theme="light"] .mageforge-toolbar-menu { + --mageforge-bg-dark: rgba(255, 255, 255, 0.98); + --mageforge-bg-dark-alt: rgba(248, 250, 252, 0.98); + --mageforge-border-color: rgba(0, 0, 0, 0.12); + --mageforge-surface-glass-hover: rgba(0, 0, 0, 0.05); + --mageforge-shadow-sm: rgba(0, 0, 0, 0.1); + --mageforge-shadow-md: rgba(0, 0, 0, 0.15); + --mageforge-shadow-lg: rgba(0, 0, 0, 0.2); + --mageforge-color-white: #0f172a; + --mageforge-color-slate-400: #64748b; +} + +.mageforge-toolbar[data-theme="light"] .mageforge-toolbar-menu-toggle { + background: var(--mageforge-toggle-bg-off-light); +} + +/* ============================================================================ + Auto Mode – Light System Preference + * NOTE: Keep these custom-property overrides in sync with the [data-theme="light"] block above. + ========================================================================== */ + +@media (prefers-color-scheme: light) { + .mageforge-toolbar[data-theme="auto"] .mageforge-toolbar-menu { + --mageforge-bg-dark: rgba(255, 255, 255, 0.98); + --mageforge-bg-dark-alt: rgba(248, 250, 252, 0.98); + --mageforge-border-color: rgba(0, 0, 0, 0.12); + --mageforge-surface-glass-hover: rgba(0, 0, 0, 0.05); + --mageforge-shadow-sm: rgba(0, 0, 0, 0.1); + --mageforge-shadow-md: rgba(0, 0, 0, 0.15); + --mageforge-shadow-lg: rgba(0, 0, 0, 0.2); + --mageforge-color-white: #0f172a; + --mageforge-color-slate-400: #64748b; + } + + .mageforge-toolbar[data-theme="auto"] .mageforge-toolbar-menu-toggle { + background: var(--mageforge-toggle-bg-off-light); + } +} diff --git a/src/view/frontend/web/css/toolbar/_variables.css b/src/view/frontend/web/css/toolbar/_variables.css new file mode 100644 index 0000000..df51203 --- /dev/null +++ b/src/view/frontend/web/css/toolbar/_variables.css @@ -0,0 +1,73 @@ +/** + * MageForge Toolbar - CSS Variables + * + * All custom properties used across the toolbar component. + * + * @package OpenForgeProject\MageForge + * @license GPL-3.0 + */ + +:root { + /* Typography */ + --mageforge-font-family: + "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, + "Helvetica Neue", Arial, sans-serif; + + /* Brand Colors */ + --mageforge-color-white: #ffffff; + --mageforge-color-blue: #3b82f6; + --mageforge-color-green: #10b981; + --mageforge-color-red: #ef4444; + --mageforge-color-slate-400: #94a3b8; + --mageforge-color-orange: #fb923c; + --mageforge-color-pink: #c850c0; + --mageforge-color-purple: #a855f7; + --mageforge-color-amber: #edb04d; + + /* Alpha variants */ + --mageforge-color-green-alpha-15: rgba(16, 185, 129, 0.15); + --mageforge-color-green-alpha-35: rgba(16, 185, 129, 0.35); + --mageforge-color-red-alpha-15: rgba(239, 68, 68, 0.15); + --mageforge-color-red-alpha-35: rgba(239, 68, 68, 0.35); + --mageforge-color-amber-alpha-15: rgba(237, 176, 77, 0.15); + --mageforge-color-amber-alpha-35: rgba(237, 176, 77, 0.35); + + /* Audit group colors */ + --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); + + /* Backgrounds */ + --mageforge-bg-dark: rgba(15, 23, 42, 0.98); + --mageforge-bg-dark-alt: rgba(30, 41, 59, 0.98); + --mageforge-border-color: rgba(148, 163, 184, 0.15); + --mageforge-surface-glass-hover: rgba(255, 255, 255, 0.08); + --mageforge-border-glass: rgba(255, 255, 255, 0.1); + + /* Shadows */ + --mageforge-shadow-sm: rgba(0, 0, 0, 0.2); + --mageforge-shadow-md: rgba(0, 0, 0, 0.3); + --mageforge-shadow-lg: rgba(0, 0, 0, 0.4); + + /* Burger button */ + --mageforge-burger-bg: rgba(15, 23, 42, 0.85); + --mageforge-burger-bg-hover: rgba(30, 41, 59, 0.95); + + /* Toggle switch */ + --mageforge-toggle-bg-off: rgba(255, 255, 255, 0.15); + --mageforge-toggle-bg-off-light: rgba(0, 0, 0, 0.15); + + /* Brand gradients */ + --gradient-brand: linear-gradient( + 43deg, + var(--mageforge-color-blue) 0%, + var(--mageforge-color-pink) 50%, + var(--mageforge-color-amber) 100% + ); + --gradient-brand-hover: linear-gradient( + 43deg, + var(--mageforge-color-blue) 0%, + var(--mageforge-color-pink) 70%, + var(--mageforge-color-amber) 100% + ); +} From b939e4b470d8d3c523d0e9746a08c86c6987bd80 Mon Sep 17 00:00:00 2001 From: Mathias Elle Date: Tue, 16 Jun 2026 18:47:30 +0200 Subject: [PATCH 02/48] feat: Implement new feature for extracting ticket numbers from branch names --- src/view/frontend/web/css/toolbar.css | 1 + .../frontend/web/css/toolbar/_buttons.css | 1 + .../frontend/web/css/toolbar/_findings.css | 160 +++ src/view/frontend/web/css/toolbar/_groups.css | 343 +++++- src/view/frontend/web/css/toolbar/_health.css | 124 +- src/view/frontend/web/css/toolbar/_menu.css | 27 +- .../frontend/web/css/toolbar/_responsive.css | 10 +- src/view/frontend/web/js/toolbar.js | 14 +- src/view/frontend/web/js/toolbar/audits.js | 30 - .../js/toolbar/audits/images-without-alt.js | 8 + src/view/frontend/web/js/toolbar/ui.js | 1078 ++++++++++------- 11 files changed, 1237 insertions(+), 559 deletions(-) create mode 100644 src/view/frontend/web/css/toolbar/_findings.css diff --git a/src/view/frontend/web/css/toolbar.css b/src/view/frontend/web/css/toolbar.css index d8df23d..ff10717 100644 --- a/src/view/frontend/web/css/toolbar.css +++ b/src/view/frontend/web/css/toolbar.css @@ -13,6 +13,7 @@ @import url('toolbar/_burger.css'); @import url('toolbar/_menu.css'); @import url('toolbar/_groups.css'); +@import url('toolbar/_findings.css'); @import url('toolbar/_highlights.css'); @import url('toolbar/_feedback.css'); @import url('toolbar/_animations.css'); diff --git a/src/view/frontend/web/css/toolbar/_buttons.css b/src/view/frontend/web/css/toolbar/_buttons.css index 898b3bf..9c1f80b 100644 --- a/src/view/frontend/web/css/toolbar/_buttons.css +++ b/src/view/frontend/web/css/toolbar/_buttons.css @@ -12,6 +12,7 @@ align-items: stretch; gap: 6px; margin-bottom: 4px; + padding: 4px 12px; } .mageforge-toolbar-menu-run-all { diff --git a/src/view/frontend/web/css/toolbar/_findings.css b/src/view/frontend/web/css/toolbar/_findings.css new file mode 100644 index 0000000..0ca0a21 --- /dev/null +++ b/src/view/frontend/web/css/toolbar/_findings.css @@ -0,0 +1,160 @@ +/** + * MageForge Toolbar - Findings List + * + * Expandable sub-list rendered beneath an active audit item. + * Each row shows the affected element's CSS selector and scrolls + * to that element on click. + * + * @package OpenForgeProject\MageForge + * @license GPL-3.0 + */ + +/* ============================================================================ + Container + ========================================================================== */ + +.mageforge-audit-findings { + display: none; + width: 100%; + padding: 2px 0; + pointer-events: none; +} + +.mageforge-audit-findings.mageforge-has-findings { + display: block; + pointer-events: auto; +} + +/* ============================================================================ + Findings toggle button + ========================================================================== */ + +.mageforge-findings-toggle { + display: inline-flex; + align-items: center; + gap: 4px; + margin: 4px 0 2px 10px; + padding: 3px 10px; + background: none; + border: 1px solid rgba(148, 163, 184, 0.2); + border-radius: 6px; + color: var(--mageforge-color-slate-400); + font-family: var(--mageforge-font-family); + font-size: 10px; + font-weight: 600; + cursor: pointer; + transition: + color 0.15s ease, + border-color 0.15s ease, + background 0.15s ease; +} + +.mageforge-findings-toggle:hover { + color: var(--mageforge-color-white); + border-color: rgba(148, 163, 184, 0.45); + background: var(--mageforge-surface-glass-hover); +} + +/* ── Findings list (hidden until open) ── */ + +.mageforge-findings-list { + display: none; + padding-top: 2px; +} + +.mageforge-audit-findings.mageforge-findings-open .mageforge-findings-list { + display: block; +} + +/* ============================================================================ + Finding row + ========================================================================== */ + +.mageforge-audit-finding { + display: flex; + align-items: center; + gap: 6px; + padding: 4px 8px 4px 14px; + cursor: pointer; + border-radius: 4px; + transition: + background 0.1s ease, + color 0.1s ease; +} + +.mageforge-audit-finding:hover { + background: var(--mageforge-surface-glass-hover); +} + +/* ── Tree connector ── */ + +.mageforge-finding-tree { + font-family: ui-monospace, "Cascadia Code", "Menlo", monospace; + font-size: 11px; + color: rgba(148, 163, 184, 0.35); + flex-shrink: 0; + user-select: none; + line-height: 1; +} + +/* ── Element selector ── */ + +.mageforge-finding-selector { + flex: 1; + font-family: ui-monospace, "Cascadia Code", "Menlo", monospace; + font-size: 11px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + color: var(--mageforge-color-slate-400); + transition: color 0.1s ease; +} + +.mageforge-audit-finding:hover .mageforge-finding-selector { + color: var(--mageforge-color-white); +} + +/* Error / warning tint */ + +.mageforge-audit-finding--error .mageforge-finding-selector { + color: rgba(239, 68, 68, 0.75); +} + +.mageforge-audit-finding--warning .mageforge-finding-selector { + color: rgba(237, 176, 77, 0.75); +} + +.mageforge-audit-finding--error:hover .mageforge-finding-selector, +.mageforge-audit-finding--warning:hover .mageforge-finding-selector { + color: var(--mageforge-color-white); +} + +/* ── Inline action hint (e.g. "Add alt text") ── */ + +.mageforge-finding-action { + font-family: var(--mageforge-font-family); + font-size: 10px; + font-weight: 500; + color: var(--mageforge-color-blue); + background: rgba(59, 130, 246, 0.12); + padding: 2px 7px; + border-radius: 4px; + white-space: nowrap; + flex-shrink: 0; + opacity: 0; + transition: opacity 0.1s ease; +} + +.mageforge-audit-finding:hover .mageforge-finding-action { + opacity: 1; +} + +/* ============================================================================ + Flash highlight applied to the target element on scroll-jump + ========================================================================== */ + +.mageforge-finding-flash { + outline: 2px solid var(--mageforge-color-blue) !important; + outline-offset: 3px !important; + border-radius: 2px; +} diff --git a/src/view/frontend/web/css/toolbar/_groups.css b/src/view/frontend/web/css/toolbar/_groups.css index 4e417a9..da6a47b 100644 --- a/src/view/frontend/web/css/toolbar/_groups.css +++ b/src/view/frontend/web/css/toolbar/_groups.css @@ -1,62 +1,205 @@ /** - * MageForge Toolbar - Menu Groups + * MageForge Toolbar - Vertical Tab Navigation * - * Styles for collapsible audit groups with headers, chevrons and group labels. + * Two-column layout: left nav sidebar + right content panels. + * The nav sidebar has Home at top, audit groups in the middle, Settings at bottom. + * Active tab gets a filled pill background in the group's accent colour. * * @package OpenForgeProject\MageForge * @license GPL-3.0 */ -.mageforge-toolbar-menu-group { - margin-bottom: 4px; +/* ============================================================================ + Tab Container (fills the space between title bar and footer) + ========================================================================== */ + +.mageforge-toolbar-tabs { + display: flex; + flex-direction: row; + flex: 1; + min-height: 0; + overflow: hidden; } -.mageforge-toolbar-menu-group-header { +/* ============================================================================ + Tab Nav (left sidebar) + ========================================================================== */ + +.mageforge-toolbar-tab-nav { display: flex; - align-items: center; - justify-content: space-between; + flex-direction: column; + width: 72px; + flex-shrink: 0; + border-right: 1px solid var(--mageforge-border-color); + padding: 6px 4px; + overflow-y: auto; + overflow-x: hidden; +} + +/* ============================================================================ + Tab Button + ========================================================================== */ + +.mageforge-toolbar-tab-btn { + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: flex-start; + gap: 5px; width: 100%; - padding: 4px 4px 4px 6px; + padding: 8px 4px; background: none; border: none; - border-bottom: 1px solid var(--mageforge-border-color); + border-radius: 8px; cursor: pointer; - margin-bottom: 4px; + color: rgba(148, 163, 184, 0.55); + font-family: var(--mageforge-font-family); + font-size: 9px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.05em; + text-align: left; + line-height: 1.2; + transition: color 0.15s ease; } -.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 { +.mageforge-toolbar-tab-btn:hover { color: var(--mageforge-color-white); } -.mageforge-toolbar-menu-group-label { - font-family: var(--mageforge-font-family); - font-size: 11px; - font-weight: 600; - text-transform: uppercase; - transition: color 0.15s ease; - letter-spacing: 0.08em; - color: var(--mageforge-color-slate-400); +/* ── Active state: icon wrapper gets the filled pill ── */ + +.mageforge-toolbar-tab-btn.mageforge-tab-active { + color: var(--mageforge-color-white); } -.mageforge-toolbar-menu-group[data-group-key="wcag"] - .mageforge-toolbar-menu-group-label { +.mageforge-toolbar-tab-btn[data-tab="home"].mageforge-tab-active .mageforge-tab-icon, +.mageforge-toolbar-tab-btn[data-tab="settings"].mageforge-tab-active .mageforge-tab-icon { + background: rgba(229, 98, 42, 0.3); + color: #fb923c; +} + +.mageforge-toolbar-tab-btn[data-tab="wcag"].mageforge-tab-active .mageforge-tab-icon { + background: rgba(168, 85, 247, 0.25); color: var(--mageforge-group-color-wcag); } -.mageforge-toolbar-menu-group[data-group-key="html-quality"] - .mageforge-toolbar-menu-group-label { +.mageforge-toolbar-tab-btn[data-tab="html-quality"].mageforge-tab-active .mageforge-tab-icon { + background: rgba(59, 130, 246, 0.25); color: var(--mageforge-group-color-html-quality); } -.mageforge-toolbar-menu-group[data-group-key="performance"] - .mageforge-toolbar-menu-group-label { +.mageforge-toolbar-tab-btn[data-tab="performance"].mageforge-tab-active .mageforge-tab-icon { + background: rgba(251, 146, 60, 0.25); color: var(--mageforge-group-color-performance); } +/* ── Tab icon wrapper (receives the pill background) ── */ + +.mageforge-tab-icon { + display: flex; + align-items: center; + justify-content: center; + width: 44px; + height: 34px; + border-radius: 8px; + line-height: 0; + transition: + background 0.15s ease, + color 0.15s ease; +} + +/* ── Tab label ── */ + +.mageforge-tab-label { + display: block; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + max-width: 100%; +} + +/* ============================================================================ + Tab Content (right column – contains all panels) + ========================================================================== */ + +.mageforge-toolbar-tab-content { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; + overflow: hidden; +} + +/* ============================================================================ + Tab Panel + ========================================================================== */ + +.mageforge-toolbar-tab-panel { + display: none; + flex-direction: column; + flex: 1; + min-height: 0; +} + +.mageforge-toolbar-tab-panel.mageforge-tab-panel-active { + display: flex; +} + +/* ── Panel header: title + score ring ── */ + +.mageforge-tab-panel-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 10px 12px 8px; + border-bottom: 1px solid var(--mageforge-border-color); + flex-shrink: 0; +} + +.mageforge-tab-panel-title { + font-family: var(--mageforge-font-family); + font-size: 15px; + font-weight: 700; + color: var(--mageforge-color-white); + margin: 0; +} + +/* ── Panel body: the scrollable audit list ── */ + +.mageforge-tab-panel-body { + flex: 1; + overflow-y: auto; + overflow-x: hidden; + padding: 4px 0; +} + +/* ── Home panel ── */ + +.mageforge-home-panel { + flex: 1; + overflow-y: auto; + padding: 8px 0 4px; +} + +.mageforge-home-hint { + font-family: var(--mageforge-font-family); + font-size: 11px; + color: var(--mageforge-color-slate-400); + text-align: center; + padding: 6px 16px 0; + margin: 0; + line-height: 1.5; +} + +.mageforge-home-hint strong { + color: var(--mageforge-color-white); +} + +/* ============================================================================ + Audit icon colours (per group) + ========================================================================== */ + .mageforge-toolbar-menu-item[data-group-key="wcag"] .mageforge-toolbar-menu-icon { color: var(--mageforge-group-color-wcag); @@ -72,22 +215,148 @@ color: var(--mageforge-group-color-performance); } -.mageforge-toolbar-menu-group-chevron { + +/* ============================================================================ + Tab Container (flex row: nav | content) + ========================================================================== */ + +.mageforge-toolbar-tabs { + display: flex; + flex-direction: row; +} + +/* ============================================================================ + Tab Nav (left column) + ========================================================================== */ + +.mageforge-toolbar-tab-nav { + display: flex; + flex-direction: column; + width: 160px; + flex-shrink: 0; + border-right: 1px solid var(--mageforge-border-color); + padding: 6px 4px; + gap: 2px; +} + +/* ============================================================================ + Tab Button + ========================================================================== */ + +.mageforge-toolbar-tab-btn { + display: flex; + flex-direction: row; + align-items: center; + gap: 7px; + width: 100%; + padding: 9px 8px; + background: none; + border: none; + border-radius: 6px; + cursor: pointer; color: var(--mageforge-color-slate-400); - line-height: 0; + font-family: var(--mageforge-font-family); + font-size: 10px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.06em; + text-align: left; + line-height: 1.3; transition: color 0.15s ease, - transform 0.2s ease; + background 0.15s ease; + position: relative; } -.mageforge-toolbar-menu-group--collapsed .mageforge-toolbar-menu-group-chevron { - transform: rotate(-90deg); +.mageforge-toolbar-tab-btn:hover { + color: var(--mageforge-color-white); + background: var(--mageforge-surface-glass-hover); } -.mageforge-toolbar-menu-group-items { - overflow: hidden; +/* Active state – per-group colour accent */ + +.mageforge-toolbar-tab-btn[data-tab="wcag"].mageforge-tab-active { + color: var(--mageforge-group-color-wcag); + background: rgba(168, 85, 247, 0.1); +} + +.mageforge-toolbar-tab-btn[data-tab="html-quality"].mageforge-tab-active { + color: var(--mageforge-group-color-html-quality); + background: rgba(59, 130, 246, 0.1); +} + +.mageforge-toolbar-tab-btn[data-tab="performance"].mageforge-tab-active { + color: var(--mageforge-group-color-performance); + background: rgba(251, 146, 60, 0.1); } -.mageforge-toolbar-menu-group--collapsed .mageforge-toolbar-menu-group-items { +/* Left-edge indicator bar on active tab */ + +.mageforge-toolbar-tab-btn.mageforge-tab-active::before { + content: ""; + position: absolute; + left: 0; + top: 20%; + height: 60%; + width: 2px; + border-radius: 2px; + background: currentColor; +} + +/* ============================================================================ + Tab Icon & Label + ========================================================================== */ + +.mageforge-tab-icon { + display: flex; + align-items: center; + flex-shrink: 0; + line-height: 0; +} + +.mageforge-tab-label { + display: block; + white-space: normal; +} + +/* ============================================================================ + Tab Content (right column) + ========================================================================== */ + +.mageforge-toolbar-tab-content { + flex: 1; + min-width: 0; + padding: 4px 0; +} + +/* ============================================================================ + Tab Panel + ========================================================================== */ + +.mageforge-toolbar-tab-panel { display: none; } + +.mageforge-toolbar-tab-panel.mageforge-tab-panel-active { + display: block; +} + +/* ============================================================================ + Audit icon colours (per group) + ========================================================================== */ + +.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); +} + diff --git a/src/view/frontend/web/css/toolbar/_health.css b/src/view/frontend/web/css/toolbar/_health.css index 7ebfc27..da846d9 100644 --- a/src/view/frontend/web/css/toolbar/_health.css +++ b/src/view/frontend/web/css/toolbar/_health.css @@ -1,12 +1,132 @@ /** - * MageForge Toolbar - Health Score Gauge + * MageForge Toolbar - Health Score Gauges * - * SVG-based health score visualization. + * Two score components: + * 1. Half-arc gauge – large display in the Home panel + * 2. Score ring – compact circular widget in audit panel headers * * @package OpenForgeProject\MageForge * @license GPL-3.0 */ +/* ============================================================================ + Half-arc gauge (Home panel) + ========================================================================== */ + +.mageforge-toolbar-health-wrapper { + display: flex; + flex-direction: column; + align-items: center; + padding: 4px 12px 6px; + gap: 2px; +} + +.mageforge-toolbar-health-gauge { + width: 130px; + height: 75px; + overflow: visible; +} + +.mageforge-health-gauge-progress { + transition: stroke-dasharray 0.7s cubic-bezier(0.4, 0, 0.2, 1); +} + +.mageforge-health-gauge-needle { + transition: + x2 0.7s cubic-bezier(0.4, 0, 0.2, 1), + y2 0.7s cubic-bezier(0.4, 0, 0.2, 1), + opacity 0.3s ease; +} + +.mageforge-toolbar-health-score-text { + text-align: center; +} + +.mageforge-toolbar-health-score-value { + font-family: var(--mageforge-font-family); + font-size: 18px; + font-weight: 700; + color: var(--mageforge-color-white); + line-height: 1.2; +} + +.mageforge-toolbar-health-score-max { + font-size: 12px; + font-weight: 400; + color: var(--mageforge-color-slate-400); +} + +.mageforge-toolbar-health-score-label { + font-family: var(--mageforge-font-family); + font-size: 10px; + font-weight: 500; + letter-spacing: 0.05em; + text-transform: uppercase; + color: var(--mageforge-color-slate-400); + margin-top: 2px; +} + +/* ============================================================================ + Score ring (audit panel headers) + ========================================================================== */ + +.mageforge-score-widget { + position: relative; + flex-shrink: 0; + width: 50px; + height: 50px; +} + +.mageforge-score-ring { + transition: stroke-dasharray 0.7s cubic-bezier(0.4, 0, 0.2, 1); +} + +/* Text overlay centred inside the SVG */ +.mageforge-score-overlay { + position: absolute; + inset: 0; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 1px; + pointer-events: none; +} + +.mageforge-score-value { + display: flex; + align-items: baseline; + gap: 1px; + line-height: 1; +} + +.mageforge-score-number { + font-family: var(--mageforge-font-family); + font-size: 10px; + font-weight: 700; + color: var(--mageforge-color-white); + line-height: 1; +} + +.mageforge-score-denom { + font-family: var(--mageforge-font-family); + font-size: 7px; + color: var(--mageforge-color-slate-400); + line-height: 1; +} + +.mageforge-score-label { + font-family: var(--mageforge-font-family); + font-size: 6px; + font-weight: 500; + letter-spacing: 0.03em; + text-transform: uppercase; + color: var(--mageforge-color-slate-400); + text-align: center; + line-height: 1; +} + + .mageforge-toolbar-health-wrapper { display: flex; flex-direction: column; diff --git a/src/view/frontend/web/css/toolbar/_menu.css b/src/view/frontend/web/css/toolbar/_menu.css index 4fd49ad..52d3b02 100644 --- a/src/view/frontend/web/css/toolbar/_menu.css +++ b/src/view/frontend/web/css/toolbar/_menu.css @@ -31,13 +31,15 @@ box-shadow: 0 -8px 24px var(--mageforge-shadow-lg), 0 6px 10px var(--mageforge-shadow-sm); - padding: 0 6px 6px; - min-width: 350px; - max-height: 90vh; - overflow-y: auto; - overflow-x: hidden; + padding: 0; + min-width: 440px; + max-height: min(90vh, 1000px); + min-height: 60vh; + overflow: hidden; font-family: var(--mageforge-font-family); + /* flex-column keeps the footer pinned; the tab section fills remaining height */ display: none; + flex-direction: column; opacity: 0; transform: translateY(8px); transition: @@ -47,7 +49,7 @@ } .mageforge-toolbar-menu.mageforge-menu-open { - display: block; + display: flex; opacity: 1; transform: translateY(0); } @@ -68,12 +70,9 @@ flex-wrap: wrap; align-items: center; justify-content: space-between; - padding: 10px 8px 2px; + padding: 10px 12px 8px; border-bottom: 1px solid var(--mageforge-border-color); - margin-bottom: 4px; - position: sticky; - top: 0; - z-index: 99999; + flex-shrink: 0; background: linear-gradient( 135deg, var(--mageforge-bg-dark) 0%, @@ -85,8 +84,7 @@ font-family: var(--mageforge-font-family); font-size: 20px; font-weight: 700; - letter-spacing: 0.08em; - padding-bottom: 8px; + padding-bottom: 2px; color: transparent; background-image: var(--gradient-brand); background-clip: text; @@ -97,7 +95,7 @@ .mageforge-toolbar-menu-logo { display: flex; align-items: center; - padding-bottom: 8px; + gap: 8px; } .mageforge-toolbar-menu-logo svg { @@ -130,6 +128,7 @@ .mageforge-toolbar-menu-item { display: flex; align-items: center; + flex-wrap: wrap; gap: 10px; width: 100%; padding: 8px 10px; diff --git a/src/view/frontend/web/css/toolbar/_responsive.css b/src/view/frontend/web/css/toolbar/_responsive.css index 8333b66..744475e 100644 --- a/src/view/frontend/web/css/toolbar/_responsive.css +++ b/src/view/frontend/web/css/toolbar/_responsive.css @@ -28,9 +28,15 @@ top: 10px; } .mageforge-toolbar-menu { - min-width: 300px; + min-width: 320px; max-height: calc(100vh - 60px); - overflow: auto; + } + .mageforge-toolbar-tab-nav { + width: 60px; + } + .mageforge-tab-icon { + width: 36px; + height: 30px; } .mageforge-toolbar-burger-label { display: none; diff --git a/src/view/frontend/web/js/toolbar.js b/src/view/frontend/web/js/toolbar.js index 5a5dc02..c1e177a 100644 --- a/src/view/frontend/web/js/toolbar.js +++ b/src/view/frontend/web/js/toolbar.js @@ -15,8 +15,8 @@ function _registerMageforgeToolbar() { /** @type {Set} Keys of currently active audits */ activeAudits: new Set(), - /** @type {Set} Keys of currently collapsed groups */ - collapsedGroups: new Set(), + /** @type {string} Key of the currently active tab */ + activeTab: "home", /** @type {HTMLDivElement|null} */ container: null, @@ -38,16 +38,6 @@ function _registerMageforgeToolbar() { // ==================================================================== init() { - try { - const saved = localStorage.getItem( - "mageforge-toolbar-collapsed-groups", - ); - if (saved) { - try { - JSON.parse(saved).forEach((key) => this.collapsedGroups.add(key)); - } catch (_) {} - } - } catch (_) {} this.createToolbar(); }, diff --git a/src/view/frontend/web/js/toolbar/audits.js b/src/view/frontend/web/js/toolbar/audits.js index c008c52..3355a88 100644 --- a/src/view/frontend/web/js/toolbar/audits.js +++ b/src/view/frontend/web/js/toolbar/audits.js @@ -131,36 +131,6 @@ export const auditMethods = { return auditGroups; }, - /** - * Toggle collapsed state of a menu group. - * - * @param {string} key - */ - toggleGroup(key) { - if (this.collapsedGroups.has(key)) { - this.collapsedGroups.delete(key); - } else { - this.collapsedGroups.add(key); - } - localStorage.setItem( - "mageforge-toolbar-collapsed-groups", - JSON.stringify([...this.collapsedGroups]), - ); - if (!this.menu) return; - const group = this.menu.querySelector(`[data-group-key="${key}"]`); - if (group) { - const isCollapsed = this.collapsedGroups.has(key); - group.classList.toggle( - "mageforge-toolbar-menu-group--collapsed", - isCollapsed, - ); - const header = group.querySelector( - ".mageforge-toolbar-menu-group-header", - ); - if (header) header.setAttribute("aria-expanded", String(!isCollapsed)); - } - }, - /** * Update the description text of an audit menu item. * Useful for audits that want to surface detail (e.g. which IDs are duplicated). diff --git a/src/view/frontend/web/js/toolbar/audits/images-without-alt.js b/src/view/frontend/web/js/toolbar/audits/images-without-alt.js index 42d3346..f40732b 100644 --- a/src/view/frontend/web/js/toolbar/audits/images-without-alt.js +++ b/src/view/frontend/web/js/toolbar/audits/images-without-alt.js @@ -18,6 +18,7 @@ export default { run(context, active) { if (!active) { clearHighlight(this.key); + context.setAuditFindings(this.key, []); return; } @@ -47,6 +48,7 @@ export default { const total = errors.length + warnings.length; if (total === 0) { context.setAuditCounterBadge(this.key, "0", "success"); + context.setAuditFindings(this.key, []); return; } @@ -56,6 +58,12 @@ export default { skipBadge: true, }); + // Populate the clickable findings list + context.setAuditFindings(this.key, [ + ...errors.map((el) => ({ el, severity: "error", action: "Add alt text" })), + ...warnings.map((el) => ({ el, severity: "warning", action: "Check alt text" })), + ]); + // Scroll to first issue (errors take priority) const first = errors[0] ?? warnings[0]; if (first && !context._batchRunning) diff --git a/src/view/frontend/web/js/toolbar/ui.js b/src/view/frontend/web/js/toolbar/ui.js index f5cd3a8..895663f 100644 --- a/src/view/frontend/web/js/toolbar/ui.js +++ b/src/view/frontend/web/js/toolbar/ui.js @@ -1,423 +1,546 @@ /** - * MageForge Toolbar - DOM construction and menu controls + * MageForge Toolbar – DOM construction and menu controls + * + * Structure: + * createToolbar() – Entry point; assembles and injects the toolbar DOM + * _buildMenu() – Full menu popup container + * _buildMenuHeader() – Sticky title bar (logo + name + close button) + * _buildTabLayout() – Two-column tab container (nav | content) + * _buildTabNav() – Left-side navigation buttons + * _buildNavTab() – Single nav tab button + * _buildTabPanels() – All content panels + * _buildPanel() – Panel shell (role=tabpanel) + * _buildPanelHeader() – Panel title + compact score ring + * _buildScoreWidget() – Circular score ring (panel headers) + * _buildHomePanel() – Overview panel with half-arc gauge + * _buildSettingsPanel() – Settings placeholder + * _buildMenuFooter() – Run All Tests + Reset + credit + * _buildBurgerButton() – Persistent trigger button + * + * switchTab() – Activate a tab and show its panel + * createMenuItem() – Single audit row + * setAuditFindings() – Populate the clickable findings list under an item + * setAuditActive() – Update an item's active / inactive visual state + * updateHealthScore() – Animate all gauges and rings to a new score + * resetScore() – Reset gauges + deactivate all audits + * toggleMenu() / openMenu() / closeMenu() / destroyToolbar() */ +// ── Constants ────────────────────────────────────────────────────────────── + const LOGO_SVG_PATH = "M176 0L0 101.614V297L176 398.614L352 297V101.614L176 0ZM39 275.5V124L76.2391 101.614L101.5 162L126.5 73.4393L164.5 51.5V346.939L126.5 325V188L108.5 239H95L76.2391 188V297L39 275.5ZM187.5 346.939V51.5L313 124V170H275.5V146.368L225.5 117.5V188H280V226.5H225.5V325L187.5 346.939Z"; +const ICON_HOME = ''; + +const GROUP_ICONS = { + "wcag": '', + "html-quality": '', + "performance": '', +}; + +// ── Module-level helpers ─────────────────────────────────────────────────── + function createLogoSvg(fill) { return ``; } +/** + * Build a short, human-readable CSS selector for labelling a finding. + * + * @param {Element} el + * @returns {string} + */ +function getReadableSelector(el) { + if (el.id) return `#${el.id}`; + const tag = el.tagName.toLowerCase(); + const classes = [...el.classList] + .filter((c) => !c.startsWith("mageforge")) + .slice(0, 2) + .join("."); + if (classes) return `${tag}.${classes}`; + const ariaLabel = el.getAttribute("aria-label"); + if (ariaLabel) return `${tag}[aria-label]`; + if (el.name) return `${tag}[name="${el.name}"]`; + if (tag === "img" && el.src) { + const base = el.src.split("/").pop().split("?")[0].slice(0, 24); + return `img/${base}`; + } + return tag; +} + +// ── Exported mixin ───────────────────────────────────────────────────────── + export const uiMethods = { - createToolbar() { - const logoSvgOrange = createLogoSvg("#E5622A"); - const logoSvgWhite = createLogoSvg("white"); + // ──────────────────────────────────────────────────────────────────────── + // Entry point + // ──────────────────────────────────────────────────────────────────────── + + /** + * Build and inject the full toolbar into . + */ + createToolbar() { this.container = document.createElement("div"); this.container.className = "mageforge-toolbar"; - if (this.$el && this.$el.hasAttribute("data-theme")) { - this.container.setAttribute( - "data-theme", - this.$el.getAttribute("data-theme"), - ); + if (this.$el?.hasAttribute("data-theme")) { + this.container.setAttribute("data-theme", this.$el.getAttribute("data-theme")); } - - if (this.$el && this.$el.hasAttribute("data-position")) { - this.container.setAttribute( - "data-position", - this.$el.getAttribute("data-position"), - ); + if (this.$el?.hasAttribute("data-position")) { + this.container.setAttribute("data-position", this.$el.getAttribute("data-position")); } - - if (this.$el && this.$el.getAttribute("data-show-labels") === "0") { + if (this.$el?.getAttribute("data-show-labels") === "0") { this.container.classList.add("mageforge-toolbar--no-labels"); } - // Menu popup (before buttons so it sits correctly in stacking context) - this.menu = document.createElement("div"); - this.menu.className = "mageforge-toolbar-menu"; - - const menuTitle = document.createElement("div"); - menuTitle.className = "mageforge-toolbar-menu-title"; - - menuTitle.innerHTML = ` - - MageForge - - `; - menuTitle.querySelector(".mageforge-toolbar-menu-close").onclick = (e) => { + this.menu = this._buildMenu(); + this.container.appendChild(this.menu); + + this.burgerButton = this._buildBurgerButton(); + this.container.appendChild(this.burgerButton); + // Note: Inspector button is appended externally via _appendInspectorButton() + + this._outsideClickHandler = (e) => { + if (this.menuOpen && !this.container.contains(e.target)) this.closeMenu(); + }; + document.addEventListener("click", this._outsideClickHandler); + document.body.appendChild(this.container); + }, + + // ──────────────────────────────────────────────────────────────────────── + // Menu sections + // ──────────────────────────────────────────────────────────────────────── + + /** + * Assemble the full menu popup. + * + * @returns {HTMLDivElement} + */ + _buildMenu() { + const menu = document.createElement("div"); + menu.className = "mageforge-toolbar-menu"; + menu.appendChild(this._buildMenuHeader()); + menu.appendChild(this._buildTabLayout()); + menu.appendChild(this._buildMenuFooter()); + return menu; + }, + + /** + * Sticky title bar: logo + name + close button. + * + * @returns {HTMLDivElement} + */ + _buildMenuHeader() { + const header = document.createElement("div"); + header.className = "mageforge-toolbar-menu-title"; + header.innerHTML = ` + + + `; + header.querySelector(".mageforge-toolbar-menu-close").onclick = (e) => { e.stopPropagation(); this.deactivateAllAudits(); this.closeMenu(); }; - this.menu.appendChild(menuTitle); + return header; + }, + + /** + * Two-column layout: tab nav (left) + content panels (right). + * + * @returns {HTMLDivElement} + */ + _buildTabLayout() { + const layout = document.createElement("div"); + layout.className = "mageforge-toolbar-tabs"; + layout.appendChild(this._buildTabNav()); + layout.appendChild(this._buildTabPanels()); + return layout; + }, + + // ──────────────────────────────────────────────────────────────────────── + // Tab navigation + // ──────────────────────────────────────────────────────────────────────── + + /** + * Left-side navigation: Home at top, audit groups, Settings pinned to bottom. + * + * @returns {HTMLElement} + */ + _buildTabNav() { + const nav = document.createElement("nav"); + nav.className = "mageforge-toolbar-tab-nav"; + nav.setAttribute("role", "tablist"); + nav.setAttribute("aria-label", "Audit categories"); + + nav.appendChild(this._buildNavTab("home", ICON_HOME, "Home", true)); + + this.getAuditGroups().forEach((group) => { + nav.appendChild(this._buildNavTab(group.key, GROUP_ICONS[group.key] ?? "", group.label)); + }); + + return nav; + }, + + /** + * Single tab button (icon stacked above abbreviated label). + * + * @param {string} key + * @param {string} icon – SVG string + * @param {string} label – Display label (first word used in the nav) + * @param {boolean} [isActive] + * @returns {HTMLButtonElement} + */ + _buildNavTab(key, icon, label, isActive = false) { + const btn = document.createElement("button"); + btn.type = "button"; + btn.setAttribute("role", "tab"); + btn.setAttribute("aria-selected", String(isActive)); + btn.setAttribute("aria-controls", `mf-tab-panel-${key}`); + btn.setAttribute("tabindex", isActive ? "0" : "-1"); + btn.className = "mageforge-toolbar-tab-btn"; + if (isActive) btn.classList.add("mageforge-tab-active"); + btn.dataset.tab = key; + btn.innerHTML = ` + + ${label.split(" ")[0]} + `; + btn.onclick = (e) => { e.stopPropagation(); this.switchTab(key); }; + btn.onkeydown = (e) => { + if (e.key === "Enter" || e.key === " ") { e.preventDefault(); e.stopPropagation(); this.switchTab(key); } + }; + return btn; + }, + + // ──────────────────────────────────────────────────────────────────────── + // Tab panels + // ──────────────────────────────────────────────────────────────────────── + + /** + * Build all content panels wrapped in the scrollable content column. + * + * @returns {HTMLDivElement} + */ + _buildTabPanels() { + const wrapper = document.createElement("div"); + wrapper.className = "mageforge-toolbar-tab-content"; + + const showHealthScore = this.$el?.getAttribute("data-show-health-score") !== "0"; - // Group audits by their group key; ungrouped items fall through const grouped = {}; const ungrouped = []; this.getAudits().forEach((audit) => { - if (audit.group) { - (grouped[audit.group] = grouped[audit.group] || []).push(audit); - } else { - ungrouped.push(audit); - } + audit.group + ? (grouped[audit.group] = grouped[audit.group] || []).push(audit) + : ungrouped.push(audit); }); - // Render defined groups in order + // Home panel – visible by default + const homePanel = this._buildPanel("home"); + homePanel.classList.add("mageforge-tab-panel-active"); + homePanel.appendChild(this._buildHomePanel(showHealthScore)); + wrapper.appendChild(homePanel); + + // One panel per audit group this.getAuditGroups().forEach((group) => { const items = grouped[group.key]; if (!items?.length) return; - this.menu.appendChild( - this.createMenuGroup(group.key, group.label, items), - ); - }); - // Render any ungrouped audits below - ungrouped.forEach((audit) => { - this.menu.appendChild( - this.createMenuItem( - audit.key, - audit.icon, - audit.label, - audit.description, - () => this.runAudit(audit.key), - ), - ); + const panel = this._buildPanel(group.key); + panel.setAttribute("hidden", ""); + panel.appendChild(this._buildPanelHeader(group.label, false, group.key)); + + const body = document.createElement("div"); + body.className = "mageforge-tab-panel-body"; + items.forEach((audit) => { + body.appendChild( + this.createMenuItem( + audit.key, audit.icon, audit.label, audit.description, + () => this.runAudit(audit.key), + group.key, + ), + ); + }); + panel.appendChild(body); + wrapper.appendChild(panel); }); - // Footer – Health Score Gauge + Run All Tests - const menuFooter = document.createElement("div"); - menuFooter.className = "mageforge-toolbar-menu-footer"; + // Ungrouped audits (no header) + if (ungrouped.length > 0) { + const panel = this._buildPanel("ungrouped"); + panel.setAttribute("hidden", ""); + ungrouped.forEach((audit) => { + panel.appendChild( + this.createMenuItem( + audit.key, audit.icon, audit.label, audit.description, + () => this.runAudit(audit.key), + ), + ); + }); + wrapper.appendChild(panel); + } - const showHealthScore = - this.$el?.getAttribute("data-show-health-score") !== "0"; + return wrapper; + }, + + /** + * Create a bare panel shell. + * + * @param {string} key + * @returns {HTMLDivElement} + */ + _buildPanel(key) { + const panel = document.createElement("div"); + panel.className = "mageforge-toolbar-tab-panel"; + panel.setAttribute("role", "tabpanel"); + panel.setAttribute("id", `mf-tab-panel-${key}`); + panel.dataset.panel = key; + return panel; + }, + + /** + * Panel title bar: group name on the left, score ring on the right. + * + * @param {string} title + * @param {boolean} showScore + * @param {string} groupKey + * @returns {HTMLDivElement} + */ + _buildPanelHeader(title, showScore, groupKey) { + const header = document.createElement("div"); + header.className = "mageforge-tab-panel-header"; + header.dataset.group = groupKey; + + const titleEl = document.createElement("h2"); + titleEl.className = "mageforge-tab-panel-title"; + titleEl.textContent = title; + header.appendChild(titleEl); + + if (showScore) header.appendChild(this._buildScoreWidget()); + return header; + }, + + /** + * Compact circular score ring shown in every audit panel header. + * + * @returns {HTMLDivElement} + */ + _buildScoreWidget() { + const CIRCUMFERENCE = 113.1; // 2pi x r=18 + const gradId = `mf-sg-${Math.random().toString(36).slice(2, 7)}`; + const widget = document.createElement("div"); + widget.className = "mageforge-score-widget"; + widget.innerHTML = ` + +
+
+ --/100 +
+
Health Score
+
+ `; + return widget; + }, + + /** + * Home panel: half-arc gauge overview + intro hint. + * + * @param {boolean} showHealthScore + * @returns {HTMLDivElement} + */ + _buildHomePanel(showHealthScore) { + const panel = document.createElement("div"); + panel.className = "mageforge-home-panel"; if (showHealthScore) { const ARC_LENGTH = 157.08; - const gradId = `mf-gauge-grad-${Math.random().toString(36).slice(2, 8)}`; - const healthWrapper = document.createElement("div"); - healthWrapper.className = "mageforge-toolbar-health-wrapper"; - - const gaugeSvg = document.createElementNS( - "http://www.w3.org/2000/svg", - "svg", - ); - gaugeSvg.setAttribute("viewBox", "0 0 120 70"); - gaugeSvg.setAttribute("class", "mageforge-toolbar-health-gauge"); - gaugeSvg.setAttribute("aria-hidden", "true"); - gaugeSvg.innerHTML = ` - - - - - - - - - - - - `; - healthWrapper.appendChild(gaugeSvg); - - const scoreTextWrapper = document.createElement("div"); - scoreTextWrapper.className = "mageforge-toolbar-health-score-text"; - scoreTextWrapper.innerHTML = ` -
- --/100 -
-
Overall Health Score
- `; - healthWrapper.appendChild(scoreTextWrapper); - menuFooter.appendChild(healthWrapper); - - // Run All Tests + Reset button row (with score) - const buttonRow = document.createElement("div"); - buttonRow.className = "mageforge-toolbar-menu-button-row"; - - 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 = ` - - RUN ALL TESTS - `; - this.runAllButton.onclick = (e) => { - e.stopPropagation(); - this.runAllAuditsForScore(); - }; - this.runAllButton.onkeydown = (e) => { - 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); - - 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", - ); - this.resetButton.innerHTML = ``; - this.resetButton.onclick = (e) => { - e.stopPropagation(); - this.resetScore(); - }; - this.resetButton.onkeydown = (e) => { - 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); - - menuFooter.appendChild(buttonRow); + const gradId = `mf-gauge-${Math.random().toString(36).slice(2, 8)}`; + panel.innerHTML = ` +
+ +
+
+ -- + /100 +
+
Overall Health Score
+
+
+

Select a category on the left or click Run All Tests below.

+ `; } else { - // No health score – button row with Run All + Reset - const buttonRow = document.createElement("div"); - buttonRow.className = "mageforge-toolbar-menu-button-row"; - - 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 = ` - - RUN ALL TESTS - `; - this.runAllButton.onclick = (e) => { - e.stopPropagation(); - this.runAllAuditsForScore(); - }; - this.runAllButton.onkeydown = (e) => { - 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); - - 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"); - this.resetButton.innerHTML = ``; - this.resetButton.onclick = (e) => { - e.stopPropagation(); - this.resetScore(); - }; - this.resetButton.onkeydown = (e) => { - 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); - - menuFooter.appendChild(buttonRow); + panel.innerHTML = `

Select a category on the left to run audits.

`; } - const credit = document.createElement("div"); - credit.className = "mageforge-toolbar-menu-credit"; - credit.innerHTML = `Built with by MageForge`; - menuFooter.appendChild(credit); - this.menu.appendChild(menuFooter); - - // Burger button (left) — div avoids Luma/theme button CSS overrides - this.burgerButton = document.createElement("div"); - this.burgerButton.className = "mageforge-toolbar-burger"; - this.burgerButton.title = "Audit tools"; - this.burgerButton.setAttribute("role", "button"); - this.burgerButton.setAttribute("tabindex", "0"); - this.burgerButton.setAttribute("aria-label", "Open audit tools menu"); - this.burgerButton.setAttribute("aria-expanded", "false"); - this.burgerButton.innerHTML = ` - - MageForge - `; - this.burgerButton.onclick = (e) => { - e.preventDefault(); - e.stopPropagation(); - this.toggleMenu(); - }; - this.burgerButton.onkeydown = (e) => { - if (e.key === "Enter") { - e.preventDefault(); - e.stopPropagation(); - this.toggleMenu(); - } - if (e.key === " ") { - e.preventDefault(); // prevent page scroll - } + return panel; + }, + + // ──────────────────────────────────────────────────────────────────────── + // Footer + // ──────────────────────────────────────────────────────────────────────── + + /** + * Footer row: Run All Tests + Reset button + credit line. + * Sets this.runAllButton and this.resetButton as side effects. + * + * @returns {HTMLDivElement} + */ + _buildMenuFooter() { + const footer = document.createElement("div"); + footer.className = "mageforge-toolbar-menu-footer"; + + // ── Button row ────────────────────────────────────────────────────── + const btnRow = document.createElement("div"); + btnRow.className = "mageforge-toolbar-menu-button-row"; + + 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 = ` + + RUN ALL TESTS + `; + this.runAllButton.onclick = (e) => { e.stopPropagation(); this.runAllAuditsForScore(); }; + this.runAllButton.onkeydown = (e) => { + if (e.key === "Enter") { e.preventDefault(); e.stopPropagation(); this.runAllAuditsForScore(); } + if (e.key === " ") { e.preventDefault(); } }; - this.burgerButton.onkeyup = (e) => { - if (e.key === " ") { - e.stopPropagation(); - this.toggleMenu(); - } + this.runAllButton.onkeyup = (e) => { if (e.key === " ") { e.stopPropagation(); this.runAllAuditsForScore(); } }; + btnRow.appendChild(this.runAllButton); + + 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"); + this.resetButton.innerHTML = ''; + this.resetButton.onclick = (e) => { e.stopPropagation(); this.resetScore(); }; + this.resetButton.onkeydown = (e) => { + 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(); } }; + btnRow.appendChild(this.resetButton); - this.container.appendChild(this.menu); - this.container.appendChild(this.burgerButton); - // Note: inspector button is appended by the mageforgeInspector Alpine component via _appendInspectorButton() + footer.appendChild(btnRow); - // Close menu when clicking outside the toolbar - this._outsideClickHandler = (e) => { - if (this.menuOpen && !this.container.contains(e.target)) { - this.closeMenu(); - } - }; - document.addEventListener("click", this._outsideClickHandler); + // ── Credit line ───────────────────────────────────────────────────── + const credit = document.createElement("div"); + credit.className = "mageforge-toolbar-menu-credit"; + credit.innerHTML = 'Built with \u2764 by MageForge'; + footer.appendChild(credit); - document.body.appendChild(this.container); + return footer; }, + // ──────────────────────────────────────────────────────────────────────── + // Burger / trigger button + // ──────────────────────────────────────────────────────────────────────── + /** - * Create a collapsible group section containing audit menu items. + * Build the persistent trigger button (logo + label). * - * @param {string} key - Group key - * @param {string} label - Display label - * @param {object[]} audits - Audit definitions belonging to this group - * @return {HTMLDivElement} + * @returns {HTMLDivElement} */ - createMenuGroup(key, label, audits) { - const group = document.createElement("div"); - group.className = "mageforge-toolbar-menu-group"; - group.dataset.groupKey = key; - - 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) => { - e.preventDefault(); - e.stopPropagation(); - this.toggleGroup(key); - }; - header.onkeydown = (e) => { - 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); - } + _buildBurgerButton() { + const btn = document.createElement("div"); + btn.className = "mageforge-toolbar-burger"; + btn.title = "Audit tools"; + btn.setAttribute("role", "button"); + btn.setAttribute("tabindex", "0"); + btn.setAttribute("aria-label", "Open audit tools menu"); + btn.setAttribute("aria-expanded", "false"); + btn.innerHTML = ` + + MageForge + `; + btn.onclick = (e) => { e.preventDefault(); e.stopPropagation(); this.toggleMenu(); }; + btn.onkeydown = (e) => { + if (e.key === "Enter") { e.preventDefault(); e.stopPropagation(); this.toggleMenu(); } + if (e.key === " ") { e.preventDefault(); } }; + btn.onkeyup = (e) => { if (e.key === " ") { e.stopPropagation(); this.toggleMenu(); } }; + return btn; + }, - const headerLabel = document.createElement("span"); - headerLabel.className = "mageforge-toolbar-menu-group-label"; - headerLabel.textContent = label; - - const chevron = document.createElement("span"); - chevron.className = "mageforge-toolbar-menu-group-chevron"; - chevron.innerHTML = ``; - - header.appendChild(headerLabel); - header.appendChild(chevron); - group.appendChild(header); - - group.classList.toggle( - "mageforge-toolbar-menu-group--collapsed", - this.collapsedGroups.has(key), - ); - - const items = document.createElement("div"); - items.className = "mageforge-toolbar-menu-group-items"; - - audits.forEach((audit) => { - items.appendChild( - this.createMenuItem( - audit.key, - audit.icon, - audit.label, - audit.description, - () => this.runAudit(audit.key), - key, - ), - ); + // ──────────────────────────────────────────────────────────────────────── + // Tab switching + // ──────────────────────────────────────────────────────────────────────── + + /** + * Activate a tab and show its panel; hide all others. + * + * @param {string} key + */ + switchTab(key) { + this.activeTab = key; + if (!this.menu) return; + + this.menu.querySelectorAll(".mageforge-toolbar-tab-btn").forEach((btn) => { + const active = btn.dataset.tab === key; + btn.classList.toggle("mageforge-tab-active", active); + btn.setAttribute("aria-selected", String(active)); + btn.setAttribute("tabindex", active ? "0" : "-1"); }); - group.appendChild(items); - return group; + this.menu.querySelectorAll(".mageforge-toolbar-tab-panel").forEach((panel) => { + const active = panel.dataset.panel === key; + panel.classList.toggle("mageforge-tab-panel-active", active); + active ? panel.removeAttribute("hidden") : panel.setAttribute("hidden", ""); + }); }, + // ──────────────────────────────────────────────────────────────────────── + // Audit items + // ──────────────────────────────────────────────────────────────────────── + /** - * Create a single audit menu item button + * Create a single audit row: icon | label + status badge | toggle | findings list. * - * @param {string} key - * @param {string} icon - * @param {string} label - * @param {string} description + * @param {string} key + * @param {string} icon + * @param {string} label + * @param {string} description * @param {Function} callback - * @param {?string} groupKey - Optional parent group key for the item - * @return {HTMLDivElement} + * @param {?string} groupKey + * @returns {HTMLDivElement} */ createMenuItem(key, icon, label, description, callback, groupKey = null) { const item = document.createElement("div"); @@ -428,149 +551,182 @@ export const uiMethods = { if (groupKey) item.dataset.groupKey = groupKey; item.setAttribute("aria-pressed", "false"); - const iconEl = document.createElement("span"); - iconEl.className = "mageforge-toolbar-menu-icon"; - iconEl.innerHTML = icon; - - const labelEl = document.createElement("span"); - labelEl.className = "mageforge-toolbar-menu-label"; - labelEl.textContent = label; + item.innerHTML = ` + ${icon} + + + ${label} + + + ${description} + + + `; + + // Findings list – populated by setAuditFindings(); clicks never bubble to the toggle + const findings = document.createElement("div"); + findings.className = "mageforge-audit-findings"; + findings.addEventListener("click", (e) => e.stopPropagation()); + item.appendChild(findings); + + item.onclick = (e) => { e.preventDefault(); e.stopPropagation(); callback(); }; + item.onkeydown = (e) => { + if (e.key === "Enter") { e.preventDefault(); e.stopPropagation(); callback(); } + if (e.key === " ") { e.preventDefault(); } + }; + item.onkeyup = (e) => { if (e.key === " ") { e.stopPropagation(); callback(); } }; - const statusEl = document.createElement("span"); - statusEl.className = "mageforge-toolbar-menu-status"; + return item; + }, - const labelRowEl = document.createElement("span"); - labelRowEl.className = "mageforge-toolbar-menu-label-row"; - labelRowEl.appendChild(labelEl); - labelRowEl.appendChild(statusEl); + /** + * Populate (or clear) the findings list beneath an audit item. + * Each row scrolls to and briefly highlights the element on click. + * + * @param {string} key + * @param {Array<{el: Element, selector?: string, severity?: 'error'|'warning', action?: string}>} findings + */ + setAuditFindings(key, findings) { + if (!this.menu) return; + const item = this.menu.querySelector(`[data-audit-key="${key}"]`); + if (!item) return; + const container = item.querySelector(".mageforge-audit-findings"); + if (!container) return; - const descEl = document.createElement("span"); - descEl.className = "mageforge-toolbar-menu-desc"; - descEl.textContent = description; - descEl.addEventListener("click", (e) => { - if (descEl.classList.contains("mageforge-active")) e.stopPropagation(); - }); - descEl.addEventListener("mousedown", (e) => { - if (descEl.classList.contains("mageforge-active")) e.stopPropagation(); - }); - const textEl = document.createElement("span"); - textEl.className = "mageforge-toolbar-menu-text"; - textEl.appendChild(labelRowEl); - textEl.appendChild(descEl); + container.innerHTML = ""; + container.classList.remove("mageforge-findings-open"); - const toggleEl = document.createElement("span"); - toggleEl.className = "mageforge-toolbar-menu-toggle"; + if (!findings?.length) { + container.classList.remove("mageforge-has-findings"); + return; + } - item.appendChild(iconEl); - item.appendChild(textEl); - item.appendChild(toggleEl); + container.classList.add("mageforge-has-findings"); - item.onclick = (e) => { - e.preventDefault(); + const toggleBtn = document.createElement("button"); + toggleBtn.type = "button"; + toggleBtn.className = "mageforge-findings-toggle"; + toggleBtn.textContent = `Show affected elements (${findings.length})`; + toggleBtn.addEventListener("click", (e) => { e.stopPropagation(); - callback(); - }; - item.onkeydown = (e) => { - if (e.key === "Enter") { - e.preventDefault(); - e.stopPropagation(); - callback(); - } - if (e.key === " ") { - e.preventDefault(); - } - }; - item.onkeyup = (e) => { - if (e.key === " ") { + const isOpen = container.classList.toggle("mageforge-findings-open"); + toggleBtn.textContent = isOpen + ? `Hide affected elements (${findings.length})` + : `Show affected elements (${findings.length})`; + }); + container.appendChild(toggleBtn); + + const list = document.createElement("div"); + list.className = "mageforge-findings-list"; + + findings.forEach(({ el, selector, severity = "error", action }, index) => { + const selectorStr = selector ?? getReadableSelector(el); + const isLast = index === findings.length - 1; + + const row = document.createElement("div"); + row.className = `mageforge-audit-finding mageforge-audit-finding--${severity}`; + row.innerHTML = ` + + ${selectorStr} + ${action ? `${action}` : ""} + `; + row.addEventListener("click", (e) => { e.stopPropagation(); - callback(); - } - }; - return item; + el.scrollIntoView({ behavior: "smooth", block: "center" }); + el.classList.add("mageforge-finding-flash"); + setTimeout(() => el.classList.remove("mageforge-finding-flash"), 1200); + }); + list.appendChild(row); + }); + + container.appendChild(list); }, /** - * Update the visual active state of an audit menu item. + * Toggle the active visual state of an audit item. + * Clears findings and status badge on deactivation. * - * @param {string} key + * @param {string} key * @param {boolean} active */ setAuditActive(key, active) { if (!this.menu) return; const item = this.menu.querySelector(`[data-audit-key="${key}"]`); if (!item) return; + item.classList.toggle("mageforge-active", active); item.setAttribute("aria-pressed", String(active)); + if (!active) { - item.classList.remove("mageforge-active--error"); + item.classList.remove("mageforge-active--error", "mageforge-active--warning"); const status = item.querySelector(".mageforge-toolbar-menu-status"); - if (status) { - status.textContent = ""; - status.className = "mageforge-toolbar-menu-status"; - } + if (status) { status.textContent = ""; status.className = "mageforge-toolbar-menu-status"; } + this.setAuditFindings(key, []); } + this.updateToggleAllButton(); }, - /** - * No-op – retained for compatibility; the Run All Tests button has no dynamic label. - */ + /** No-op – retained for compatibility. */ updateToggleAllButton() {}, - /** - * Reset the health score gauge and deactivate all audits. - */ - resetScore() { - this.deactivateAllAudits(); - if (!this.menu) return; - const ARC_LENGTH = 157.08; - const progress = this.menu.querySelector( - ".mageforge-health-gauge-progress", - ); - const needle = this.menu.querySelector(".mageforge-health-gauge-needle"); - const numberEl = this.menu.querySelector( - ".mageforge-toolbar-health-score-number", - ); - if (numberEl) numberEl.textContent = "--"; - if (progress) progress.setAttribute("stroke-dasharray", `0 ${ARC_LENGTH}`); - if (needle) needle.setAttribute("opacity", "0"); - }, + // ──────────────────────────────────────────────────────────────────────── + // Health score + // ──────────────────────────────────────────────────────────────────────── /** - * Update the health score gauge and numeric display (0–100). + * Animate all score gauges and rings to the given score (0-100). * * @param {number} score */ updateHealthScore(score) { if (!this.menu) return; - const ARC_LENGTH = 157.08; - const progress = this.menu.querySelector( - ".mageforge-health-gauge-progress", - ); - const needle = this.menu.querySelector(".mageforge-health-gauge-needle"); - const numberEl = this.menu.querySelector( - ".mageforge-toolbar-health-score-number", - ); - - if (numberEl) numberEl.textContent = score; - - if (progress) { - const dash = ((score / 100) * ARC_LENGTH).toFixed(2); - progress.setAttribute("stroke-dasharray", `${dash} ${ARC_LENGTH}`); - } + const ARC_LENGTH = 157.08; + const CIRCUMFERENCE = 113.1; + // Half-arc gauge in the Home panel + const progress = this.menu.querySelector(".mageforge-health-gauge-progress"); + const needle = this.menu.querySelector(".mageforge-health-gauge-needle"); + if (progress) progress.setAttribute("stroke-dasharray", `${((score / 100) * ARC_LENGTH).toFixed(2)} ${ARC_LENGTH}`); if (needle) { const rad = (1 - score / 100) * Math.PI; needle.setAttribute("x2", (60 + 45 * Math.cos(rad)).toFixed(1)); needle.setAttribute("y2", (65 - 45 * Math.sin(rad)).toFixed(1)); needle.setAttribute("opacity", "1"); } + this.menu.querySelectorAll(".mageforge-toolbar-health-score-number").forEach((el) => { el.textContent = score; }); + + // Circular rings in audit panel headers + this.menu.querySelectorAll(".mageforge-score-ring").forEach((ring) => { + ring.setAttribute("stroke-dasharray", `${((score / 100) * CIRCUMFERENCE).toFixed(2)} ${CIRCUMFERENCE}`); + }); + this.menu.querySelectorAll(".mageforge-score-number").forEach((el) => { el.textContent = score; }); }, - toggleMenu() { - this.menuOpen ? this.closeMenu() : this.openMenu(); + /** + * Reset all score displays and deactivate all audits. + */ + resetScore() { + this.deactivateAllAudits(); + if (!this.menu) return; + const ARC_LENGTH = 157.08; + const CIRCUMFERENCE = 113.1; + + const progress = this.menu.querySelector(".mageforge-health-gauge-progress"); + const needle = this.menu.querySelector(".mageforge-health-gauge-needle"); + if (progress) progress.setAttribute("stroke-dasharray", `0 ${ARC_LENGTH}`); + if (needle) needle.setAttribute("opacity", "0"); + this.menu.querySelectorAll(".mageforge-toolbar-health-score-number").forEach((el) => { el.textContent = "--"; }); + this.menu.querySelectorAll(".mageforge-score-ring").forEach((ring) => { ring.setAttribute("stroke-dasharray", `0 ${CIRCUMFERENCE}`); }); + this.menu.querySelectorAll(".mageforge-score-number").forEach((el) => { el.textContent = "--"; }); }, + // ──────────────────────────────────────────────────────────────────────── + // Menu open / close + // ──────────────────────────────────────────────────────────────────────── + + toggleMenu() { this.menuOpen ? this.closeMenu() : this.openMenu(); }, + openMenu() { this.menuOpen = true; this.menu.classList.add("mageforge-menu-open"); @@ -590,14 +746,12 @@ export const uiMethods = { document.removeEventListener("click", this._outsideClickHandler); this._outsideClickHandler = null; } - if (this.container && this.container.parentNode) { - this.container.parentNode.removeChild(this.container); - } - this.container = null; - this.menu = null; + if (this.container?.parentNode) this.container.parentNode.removeChild(this.container); + this.container = null; + this.menu = null; this.burgerButton = null; this.runAllButton = null; - this.resetButton = null; - this.menuOpen = false; + this.resetButton = null; + this.menuOpen = false; }, }; From 608cfa8989334a511e347b6734ff4de684371f2b Mon Sep 17 00:00:00 2001 From: Mathias Elle Date: Wed, 17 Jun 2026 00:13:11 +0200 Subject: [PATCH 03/48] feat: Redesign toolbar styles for feedback, findings, groups, highlights, and menu components --- .../frontend/web/css/toolbar/_feedback.css | 18 +- .../frontend/web/css/toolbar/_findings.css | 15 +- src/view/frontend/web/css/toolbar/_groups.css | 262 +++++------------- .../frontend/web/css/toolbar/_highlights.css | 12 +- src/view/frontend/web/css/toolbar/_menu.css | 20 +- 5 files changed, 115 insertions(+), 212 deletions(-) diff --git a/src/view/frontend/web/css/toolbar/_feedback.css b/src/view/frontend/web/css/toolbar/_feedback.css index d73f6c0..f049f80 100644 --- a/src/view/frontend/web/css/toolbar/_feedback.css +++ b/src/view/frontend/web/css/toolbar/_feedback.css @@ -25,14 +25,20 @@ .mageforge-toolbar-feedback--success { color: var(--mageforge-color-green); - background: var(--mageforge-color-green-alpha-15); - border: 1px solid var(--mageforge-color-green-alpha-35); - box-shadow: 0 4px 12px var(--mageforge-color-green-alpha-15); + background: transparent; + border-left: 3px solid var(--mageforge-color-green); + border-top: none; + border-right: none; + border-bottom: none; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); } .mageforge-toolbar-feedback--error { color: var(--mageforge-color-red); - background: var(--mageforge-color-red-alpha-15); - border: 1px solid var(--mageforge-color-red-alpha-35); - box-shadow: 0 4px 12px var(--mageforge-color-red-alpha-15); + background: transparent; + border-left: 3px solid var(--mageforge-color-red); + border-top: none; + border-right: none; + border-bottom: none; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); } diff --git a/src/view/frontend/web/css/toolbar/_findings.css b/src/view/frontend/web/css/toolbar/_findings.css index 0ca0a21..98914fe 100644 --- a/src/view/frontend/web/css/toolbar/_findings.css +++ b/src/view/frontend/web/css/toolbar/_findings.css @@ -18,6 +18,7 @@ width: 100%; padding: 2px 0; pointer-events: none; + margin-left: 36px; } .mageforge-audit-findings.mageforge-has-findings { @@ -38,9 +39,9 @@ background: none; border: 1px solid rgba(148, 163, 184, 0.2); border-radius: 6px; - color: var(--mageforge-color-slate-400); + font-family: var(--mageforge-font-family); - font-size: 10px; + font-size: 11px; font-weight: 600; cursor: pointer; transition: @@ -49,6 +50,15 @@ background 0.15s ease; } +.mageforge-toolbar-menu-item.mageforge-active--error .mageforge-findings-toggle { + color: var(--mageforge-color-red); + border-color: var(--mageforge-color-red); +} +.mageforge-toolbar-menu-item.mageforge-active--warning .mageforge-findings-toggle { + color: var(--mageforge-color-amber); + border-color: var(--mageforge-color-amber); +} + .mageforge-findings-toggle:hover { color: var(--mageforge-color-white); border-color: rgba(148, 163, 184, 0.45); @@ -60,6 +70,7 @@ .mageforge-findings-list { display: none; padding-top: 2px; + padding-bottom: 8px; } .mageforge-audit-findings.mageforge-findings-open .mageforge-findings-list { diff --git a/src/view/frontend/web/css/toolbar/_groups.css b/src/view/frontend/web/css/toolbar/_groups.css index da6a47b..7effa26 100644 --- a/src/view/frontend/web/css/toolbar/_groups.css +++ b/src/view/frontend/web/css/toolbar/_groups.css @@ -10,30 +10,26 @@ */ /* ============================================================================ - Tab Container (fills the space between title bar and footer) + Tab Container (flex row: nav | content) ========================================================================== */ .mageforge-toolbar-tabs { display: flex; flex-direction: row; - flex: 1; - min-height: 0; - overflow: hidden; } /* ============================================================================ - Tab Nav (left sidebar) + Tab Nav (left column) ========================================================================== */ .mageforge-toolbar-tab-nav { display: flex; flex-direction: column; - width: 72px; + width: auto; flex-shrink: 0; border-right: 1px solid var(--mageforge-border-color); padding: 6px 4px; - overflow-y: auto; - overflow-x: hidden; + gap: 2px; } /* ============================================================================ @@ -42,108 +38,88 @@ .mageforge-toolbar-tab-btn { display: flex; - flex-direction: column; - align-items: flex-start; - justify-content: flex-start; - gap: 5px; + flex-direction: row; + align-items: center; + gap: 8px; width: 100%; - padding: 8px 4px; + padding: 8px; background: none; border: none; - border-radius: 8px; + border-radius: 6px; cursor: pointer; - color: rgba(148, 163, 184, 0.55); + color: var(--mageforge-color-slate-400); font-family: var(--mageforge-font-family); - font-size: 9px; + font-size: 11px; font-weight: 600; text-transform: uppercase; - letter-spacing: 0.05em; + letter-spacing: 0.06em; text-align: left; - line-height: 1.2; - transition: color 0.15s ease; + line-height: 1.3; + transition: + color 0.15s ease, + background 0.15s ease; + position: relative; } .mageforge-toolbar-tab-btn:hover { color: var(--mageforge-color-white); + background: var(--mageforge-surface-glass-hover); } -/* ── Active state: icon wrapper gets the filled pill ── */ - -.mageforge-toolbar-tab-btn.mageforge-tab-active { - color: var(--mageforge-color-white); -} - -.mageforge-toolbar-tab-btn[data-tab="home"].mageforge-tab-active .mageforge-tab-icon, -.mageforge-toolbar-tab-btn[data-tab="settings"].mageforge-tab-active .mageforge-tab-icon { - background: rgba(229, 98, 42, 0.3); - color: #fb923c; -} +/* Active state – per-group colour accent */ -.mageforge-toolbar-tab-btn[data-tab="wcag"].mageforge-tab-active .mageforge-tab-icon { - background: rgba(168, 85, 247, 0.25); +.mageforge-toolbar-tab-btn[data-tab="wcag"].mageforge-tab-active { color: var(--mageforge-group-color-wcag); + background: rgba(168, 85, 247, 0.1); } -.mageforge-toolbar-tab-btn[data-tab="html-quality"].mageforge-tab-active .mageforge-tab-icon { - background: rgba(59, 130, 246, 0.25); +.mageforge-toolbar-tab-btn[data-tab="html-quality"].mageforge-tab-active { color: var(--mageforge-group-color-html-quality); + background: rgba(59, 130, 246, 0.1); } -.mageforge-toolbar-tab-btn[data-tab="performance"].mageforge-tab-active .mageforge-tab-icon { - background: rgba(251, 146, 60, 0.25); +.mageforge-toolbar-tab-btn[data-tab="performance"].mageforge-tab-active { color: var(--mageforge-group-color-performance); + background: rgba(251, 146, 60, 0.1); +} + +/* Left-edge indicator bar on active tab */ + +.mageforge-toolbar-tab-btn.mageforge-tab-active::before { + content: ""; + position: absolute; + left: 0; + top: 20%; + height: 60%; + width: 2px; + border-radius: 2px; + background: currentColor; } -/* ── Tab icon wrapper (receives the pill background) ── */ +/* ============================================================================ + Tab Icon & Label + ========================================================================== */ .mageforge-tab-icon { display: flex; align-items: center; - justify-content: center; - width: 44px; - height: 34px; - border-radius: 8px; + flex-shrink: 0; line-height: 0; - transition: - background 0.15s ease, - color 0.15s ease; } -/* ── Tab label ── */ - .mageforge-tab-label { display: block; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - max-width: 100%; + white-space: normal; } /* ============================================================================ - Tab Content (right column – contains all panels) + Tab Content (right column) ========================================================================== */ .mageforge-toolbar-tab-content { flex: 1; min-width: 0; - display: flex; - flex-direction: column; - overflow: hidden; -} - -/* ============================================================================ - Tab Panel - ========================================================================== */ - -.mageforge-toolbar-tab-panel { - display: none; - flex-direction: column; - flex: 1; - min-height: 0; -} - -.mageforge-toolbar-tab-panel.mageforge-tab-panel-active { - display: flex; + padding: 4px 0; } /* ── Panel header: title + score ring ── */ @@ -171,7 +147,8 @@ flex: 1; overflow-y: auto; overflow-x: hidden; - padding: 4px 0; + padding: 8px; + gap: 8px; } /* ── Home panel ── */ @@ -196,137 +173,48 @@ color: var(--mageforge-color-white); } -/* ============================================================================ - Audit icon colours (per group) - ========================================================================== */ - -.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); -} - - -/* ============================================================================ - Tab Container (flex row: nav | content) - ========================================================================== */ - -.mageforge-toolbar-tabs { - display: flex; - flex-direction: row; -} - -/* ============================================================================ - Tab Nav (left column) - ========================================================================== */ +/* ── Home panel ── */ -.mageforge-toolbar-tab-nav { - display: flex; - flex-direction: column; - width: 160px; - flex-shrink: 0; - border-right: 1px solid var(--mageforge-border-color); - padding: 6px 4px; - gap: 2px; +.mageforge-home-panel { + flex: 1; + overflow-y: auto; + padding: 8px 0 4px; } -/* ============================================================================ - Tab Button - ========================================================================== */ - -.mageforge-toolbar-tab-btn { - display: flex; - flex-direction: row; - align-items: center; - gap: 7px; - width: 100%; - padding: 9px 8px; - background: none; - border: none; - border-radius: 6px; - cursor: pointer; - color: var(--mageforge-color-slate-400); +.mageforge-home-hint { font-family: var(--mageforge-font-family); - font-size: 10px; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.06em; - text-align: left; - line-height: 1.3; - transition: - color 0.15s ease, - background 0.15s ease; - position: relative; + font-size: 11px; + color: var(--mageforge-color-slate-400); + text-align: center; + padding: 6px 16px 0; + margin: 0; + line-height: 1.5; } -.mageforge-toolbar-tab-btn:hover { +.mageforge-home-hint strong { color: var(--mageforge-color-white); - background: var(--mageforge-surface-glass-hover); -} - -/* Active state – per-group colour accent */ - -.mageforge-toolbar-tab-btn[data-tab="wcag"].mageforge-tab-active { - color: var(--mageforge-group-color-wcag); - background: rgba(168, 85, 247, 0.1); -} - -.mageforge-toolbar-tab-btn[data-tab="html-quality"].mageforge-tab-active { - color: var(--mageforge-group-color-html-quality); - background: rgba(59, 130, 246, 0.1); } -.mageforge-toolbar-tab-btn[data-tab="performance"].mageforge-tab-active { - color: var(--mageforge-group-color-performance); - background: rgba(251, 146, 60, 0.1); -} - -/* Left-edge indicator bar on active tab */ - -.mageforge-toolbar-tab-btn.mageforge-tab-active::before { - content: ""; - position: absolute; - left: 0; - top: 20%; - height: 60%; - width: 2px; - border-radius: 2px; - background: currentColor; -} - -/* ============================================================================ - Tab Icon & Label - ========================================================================== */ +/* ── Home panel ── */ -.mageforge-tab-icon { - display: flex; - align-items: center; - flex-shrink: 0; - line-height: 0; +.mageforge-home-panel { + flex: 1; + overflow-y: auto; + padding: 8px 0 4px; } -.mageforge-tab-label { - display: block; - white-space: normal; +.mageforge-home-hint { + font-family: var(--mageforge-font-family); + font-size: 11px; + color: var(--mageforge-color-slate-400); + text-align: center; + padding: 6px 16px 0; + margin: 0; + line-height: 1.5; } -/* ============================================================================ - Tab Content (right column) - ========================================================================== */ - -.mageforge-toolbar-tab-content { - flex: 1; - min-width: 0; - padding: 4px 0; +.mageforge-home-hint strong { + color: var(--mageforge-color-white); } /* ============================================================================ diff --git a/src/view/frontend/web/css/toolbar/_highlights.css b/src/view/frontend/web/css/toolbar/_highlights.css index 96b581c..96938c1 100644 --- a/src/view/frontend/web/css/toolbar/_highlights.css +++ b/src/view/frontend/web/css/toolbar/_highlights.css @@ -2,6 +2,8 @@ * MageForge Toolbar - Audit Highlights * * Fixed-position overlay injected over every highlighted element. + * Uses subtle left-border indicators instead of full background fills + * to avoid overwhelming the page content. * * @package OpenForgeProject\MageForge * @license GPL-3.0 @@ -10,14 +12,16 @@ .mageforge-audit-overlay { position: fixed; pointer-events: none; - background-color: var(--mageforge-color-red-alpha-35); - outline: 3px solid var(--mageforge-color-red); - outline-offset: 0; + background-color: transparent; + outline: 2px solid var(--mageforge-color-red); + outline-offset: -2px; + border-left: 4px solid var(--mageforge-color-red); z-index: 9999997; } .mageforge-audit-overlay--warning { - background-color: var(--mageforge-color-amber-alpha-35); + background-color: transparent; outline-color: var(--mageforge-color-amber); outline-style: dashed; + border-left-color: var(--mageforge-color-amber); } diff --git a/src/view/frontend/web/css/toolbar/_menu.css b/src/view/frontend/web/css/toolbar/_menu.css index 52d3b02..81c3379 100644 --- a/src/view/frontend/web/css/toolbar/_menu.css +++ b/src/view/frontend/web/css/toolbar/_menu.css @@ -32,7 +32,7 @@ 0 -8px 24px var(--mageforge-shadow-lg), 0 6px 10px var(--mageforge-shadow-sm); padding: 0; - min-width: 440px; + min-width: 550px; max-height: min(90vh, 1000px); min-height: 60vh; overflow: hidden; @@ -149,8 +149,7 @@ } .mageforge-toolbar-menu-item.mageforge-active { - background: var(--mageforge-color-green-alpha-15); - border-color: var(--mageforge-color-green-alpha-35); + background: linear-gradient(90deg, var(--mageforge-color-green-alpha-15) 0%, transparent 100px); } .mageforge-toolbar-menu-item.mageforge-active .mageforge-toolbar-menu-label { @@ -158,8 +157,7 @@ } .mageforge-toolbar-menu-item.mageforge-active--error { - background: var(--mageforge-color-red-alpha-15); - border-color: var(--mageforge-color-red-alpha-35); + background: linear-gradient(90deg, var(--mageforge-color-red-alpha-15) 0%, transparent 100px); } .mageforge-toolbar-menu-item.mageforge-active--error @@ -168,8 +166,7 @@ } .mageforge-toolbar-menu-item.mageforge-active--warning { - background: var(--mageforge-color-amber-alpha-15); - border-color: var(--mageforge-color-amber-alpha-35); + background: linear-gradient(90deg, var(--mageforge-color-amber-alpha-15) 0%, transparent 100px); } .mageforge-toolbar-menu-item.mageforge-active--warning @@ -289,18 +286,15 @@ .mageforge-toolbar-menu-status--success { color: var(--mageforge-color-green); - background: var(--mageforge-color-green-alpha-15); - border: 1px solid var(--mageforge-color-green-alpha-35); + background: transparent; } .mageforge-toolbar-menu-status--error { color: var(--mageforge-color-red); - background: var(--mageforge-color-red-alpha-15); - border: 1px solid var(--mageforge-color-red-alpha-35); + background: transparent; } .mageforge-toolbar-menu-status--warning { color: var(--mageforge-color-amber); - background: var(--mageforge-color-amber-alpha-15); - border: 1px solid var(--mageforge-color-amber-alpha-35); + background: transparent; } From 628ef41f6afb0ff2e8ecc366e64233d7c8f3e814 Mon Sep 17 00:00:00 2001 From: Mathias Elle Date: Wed, 17 Jun 2026 00:40:09 +0200 Subject: [PATCH 04/48] refactor(audits): migrate audits to use createAudit function for better structure and maintainability --- src/view/frontend/web/css/toolbar.css | 32 +-- .../frontend/web/css/toolbar/_findings.css | 8 +- src/view/frontend/web/css/toolbar/_groups.css | 1 - src/view/frontend/web/css/toolbar/_health.css | 1 - src/view/frontend/web/css/toolbar/_menu.css | 18 +- .../js/toolbar/audits/buttons-without-type.js | 63 ++-- .../web/js/toolbar/audits/createAudit.js | 98 +++++++ .../web/js/toolbar/audits/duplicate-ids.js | 64 +++-- .../js/toolbar/audits/empty-interactive.js | 107 ++++--- .../web/js/toolbar/audits/highlight.js | 38 +++ .../js/toolbar/audits/images-without-alt.js | 62 +--- .../audits/images-without-dimensions.js | 57 ++-- .../audits/images-without-lazy-load.js | 61 ++-- .../js/toolbar/audits/inputs-without-label.js | 33 +-- .../js/toolbar/audits/low-contrast-text.js | 32 +-- .../web/js/toolbar/audits/multiple-h1.js | 37 +-- .../js/toolbar/audits/small-touch-targets.js | 63 ++-- .../js/toolbar/audits/unsafe-blank-target.js | 63 ++-- src/view/frontend/web/js/toolbar/ui.js | 271 +++++++++++++----- 19 files changed, 620 insertions(+), 489 deletions(-) create mode 100644 src/view/frontend/web/js/toolbar/audits/createAudit.js diff --git a/src/view/frontend/web/css/toolbar.css b/src/view/frontend/web/css/toolbar.css index ff10717..6a99b0a 100644 --- a/src/view/frontend/web/css/toolbar.css +++ b/src/view/frontend/web/css/toolbar.css @@ -8,19 +8,19 @@ * @license GPL-3.0 */ -@import url('toolbar/_variables.css'); -@import url('toolbar/_reset.css'); -@import url('toolbar/_burger.css'); -@import url('toolbar/_menu.css'); -@import url('toolbar/_groups.css'); -@import url('toolbar/_findings.css'); -@import url('toolbar/_highlights.css'); -@import url('toolbar/_feedback.css'); -@import url('toolbar/_animations.css'); -@import url('toolbar/_footer.css'); -@import url('toolbar/_health.css'); -@import url('toolbar/_buttons.css'); -@import url('toolbar/_credit.css'); -@import url('toolbar/_positions.css'); -@import url('toolbar/_themes.css'); -@import url('toolbar/_responsive.css'); +@import url("toolbar/_variables.css"); +@import url("toolbar/_reset.css"); +@import url("toolbar/_burger.css"); +@import url("toolbar/_menu.css"); +@import url("toolbar/_groups.css"); +@import url("toolbar/_findings.css"); +@import url("toolbar/_highlights.css"); +@import url("toolbar/_feedback.css"); +@import url("toolbar/_animations.css"); +@import url("toolbar/_footer.css"); +@import url("toolbar/_health.css"); +@import url("toolbar/_buttons.css"); +@import url("toolbar/_credit.css"); +@import url("toolbar/_positions.css"); +@import url("toolbar/_themes.css"); +@import url("toolbar/_responsive.css"); diff --git a/src/view/frontend/web/css/toolbar/_findings.css b/src/view/frontend/web/css/toolbar/_findings.css index 98914fe..9ec4e41 100644 --- a/src/view/frontend/web/css/toolbar/_findings.css +++ b/src/view/frontend/web/css/toolbar/_findings.css @@ -50,11 +50,13 @@ background 0.15s ease; } -.mageforge-toolbar-menu-item.mageforge-active--error .mageforge-findings-toggle { +.mageforge-toolbar-menu-item.mageforge-active--error + .mageforge-findings-toggle { color: var(--mageforge-color-red); border-color: var(--mageforge-color-red); } -.mageforge-toolbar-menu-item.mageforge-active--warning .mageforge-findings-toggle { +.mageforge-toolbar-menu-item.mageforge-active--warning + .mageforge-findings-toggle { color: var(--mageforge-color-amber); border-color: var(--mageforge-color-amber); } @@ -165,7 +167,7 @@ ========================================================================== */ .mageforge-finding-flash { - outline: 2px solid var(--mageforge-color-blue) !important; + outline: 2px dashed #ef4444 !important; outline-offset: 3px !important; border-radius: 2px; } diff --git a/src/view/frontend/web/css/toolbar/_groups.css b/src/view/frontend/web/css/toolbar/_groups.css index 7effa26..4ed9a69 100644 --- a/src/view/frontend/web/css/toolbar/_groups.css +++ b/src/view/frontend/web/css/toolbar/_groups.css @@ -247,4 +247,3 @@ .mageforge-toolbar-menu-icon { color: var(--mageforge-group-color-performance); } - diff --git a/src/view/frontend/web/css/toolbar/_health.css b/src/view/frontend/web/css/toolbar/_health.css index da846d9..8157810 100644 --- a/src/view/frontend/web/css/toolbar/_health.css +++ b/src/view/frontend/web/css/toolbar/_health.css @@ -126,7 +126,6 @@ line-height: 1; } - .mageforge-toolbar-health-wrapper { display: flex; flex-direction: column; diff --git a/src/view/frontend/web/css/toolbar/_menu.css b/src/view/frontend/web/css/toolbar/_menu.css index 81c3379..dd9e2b2 100644 --- a/src/view/frontend/web/css/toolbar/_menu.css +++ b/src/view/frontend/web/css/toolbar/_menu.css @@ -149,7 +149,11 @@ } .mageforge-toolbar-menu-item.mageforge-active { - background: linear-gradient(90deg, var(--mageforge-color-green-alpha-15) 0%, transparent 100px); + background: linear-gradient( + 90deg, + var(--mageforge-color-green-alpha-15) 0%, + transparent 100px + ); } .mageforge-toolbar-menu-item.mageforge-active .mageforge-toolbar-menu-label { @@ -157,7 +161,11 @@ } .mageforge-toolbar-menu-item.mageforge-active--error { - background: linear-gradient(90deg, var(--mageforge-color-red-alpha-15) 0%, transparent 100px); + background: linear-gradient( + 90deg, + var(--mageforge-color-red-alpha-15) 0%, + transparent 100px + ); } .mageforge-toolbar-menu-item.mageforge-active--error @@ -166,7 +174,11 @@ } .mageforge-toolbar-menu-item.mageforge-active--warning { - background: linear-gradient(90deg, var(--mageforge-color-amber-alpha-15) 0%, transparent 100px); + background: linear-gradient( + 90deg, + var(--mageforge-color-amber-alpha-15) 0%, + transparent 100px + ); } .mageforge-toolbar-menu-item.mageforge-active--warning diff --git a/src/view/frontend/web/js/toolbar/audits/buttons-without-type.js b/src/view/frontend/web/js/toolbar/audits/buttons-without-type.js index 9b8aab5..2bbbdbf 100644 --- a/src/view/frontend/web/js/toolbar/audits/buttons-without-type.js +++ b/src/view/frontend/web/js/toolbar/audits/buttons-without-type.js @@ -6,43 +6,30 @@ * type="submit", or type="reset" explicitly. */ -import { applyHighlight, clearHighlight } from "./highlight.js"; +import { createAudit } from "./createAudit.js"; -/** @type {import('./index.js').AuditDefinition} */ -export default { - key: "buttons-without-type", - icon: '', - label: "Buttons without a type", - description: - "Highlight a button missing an explicit type attribute (defaults to submit)", - - /** - * @param {object} context - Alpine toolbar component instance - * @param {boolean} active - true = activate, false = deactivate - */ - run(context, active) { - if (!active) { - clearHighlight(this.key); - return; - } - - const buttons = Array.from(document.querySelectorAll("button")).filter( - (btn) => { - const type = btn.getAttribute("type"); - if (type !== null && type.trim() !== "") return false; - if (!btn.offsetParent && getComputedStyle(btn).position !== "fixed") - return false; - const style = getComputedStyle(btn); - if ( - style.visibility === "hidden" || - style.display === "none" || - parseFloat(style.opacity) === 0 - ) - return false; - return true; - }, - ); - - applyHighlight(buttons, this.key, context); +export default createAudit( + { + key: "buttons-without-type", + icon: '', + label: "Buttons without a type", + description: + "Highlight a button missing an explicit type attribute (defaults to submit)", + }, + () => { + return Array.from(document.querySelectorAll("button")).filter((btn) => { + const type = btn.getAttribute("type"); + if (type !== null && type.trim() !== "") return false; + if (!btn.offsetParent && getComputedStyle(btn).position !== "fixed") + return false; + const style = getComputedStyle(btn); + if ( + style.visibility === "hidden" || + style.display === "none" || + parseFloat(style.opacity) === 0 + ) + return false; + return true; + }); }, -}; +); diff --git a/src/view/frontend/web/js/toolbar/audits/createAudit.js b/src/view/frontend/web/js/toolbar/audits/createAudit.js new file mode 100644 index 0000000..b9386b9 --- /dev/null +++ b/src/view/frontend/web/js/toolbar/audits/createAudit.js @@ -0,0 +1,98 @@ +/** + * MageForge Toolbar – Audit factory + * + * Reduces boilerplate in audit files. Each audit provides: + * - key, icon, label, description (metadata) + * - detect(context) → Element[] | { errors: Element[], warnings: Element[] } + * + * The factory handles the common activate/deactivate cycle: + * clearHighlight → detect → applyHighlight + * + * Optional onComplete callback for post-processing (e.g. dynamic descriptions): + * onComplete(context, elements) + */ + +import { applyHighlight, clearHighlight } from "./highlight.js"; + +/** + * @param {{ key: string, icon: string, label: string, description: string }} meta + * @param {(context: object) => Element[] | { errors: Element[], warnings: Element[] }} detect - Returns elements to highlight + * @param {(context: object, elements: Element[] | { errors: Element[], warnings: Element[] }) => void} [onComplete] - Optional post-processing callback + * @returns {{ key: string, icon: string, label: string, description: string, run: (context: object, active: boolean) => void }} + */ +export function createAudit(meta, detect, onComplete) { + const { key, icon, label, description } = meta; + + /** @type {(context: object, active: boolean) => void} */ + const run = (context, active) => { + if (!active) { + clearHighlight(key); + return; + } + + const result = detect(context); + + // Support error/warning split: { errors: Element[], warnings: Element[] } + if ( + result && + typeof result === "object" && + "errors" in result && + "warnings" in result + ) { + const { errors, warnings } = result; + + const hasErrors = errors.length > 0; + const hasWarnings = warnings.length > 0; + const total = errors.length + warnings.length; + + if (total === 0) { + context.setAuditCounterBadge(key, "0", "success"); + return; + } + + if (hasErrors) { + applyHighlight(errors, key, context, { + severity: "error", + autoFindings: true, + formatFinding: () => ({ action: "Show affected element" }), + }); + } + if (hasWarnings) { + applyHighlight(warnings, key, context, { + severity: "warning", + autoFindings: true, + formatFinding: () => ({ action: "Show affected element" }), + }); + } + + // Scroll to first issue + const first = errors[0] ?? warnings[0]; + if (first && !context._batchRunning) { + first.scrollIntoView({ behavior: "smooth", block: "center" }); + } + + context.setAuditCounterBadge( + key, + `${total}`, + hasErrors ? "error" : "warning", + ); + } else { + /** @type {Element[]} */ + const elements = result; + + if (elements.length === 0) { + context.setAuditCounterBadge(key, "0", "success"); + return; + } + + applyHighlight(elements, key, context, { + autoFindings: true, + formatFinding: () => ({ action: "Show affected element" }), + }); + } + + onComplete?.(context, result); + }; + + return { key, icon, label, description, run }; +} diff --git a/src/view/frontend/web/js/toolbar/audits/duplicate-ids.js b/src/view/frontend/web/js/toolbar/audits/duplicate-ids.js index 55c7db6..12f4561 100644 --- a/src/view/frontend/web/js/toolbar/audits/duplicate-ids.js +++ b/src/view/frontend/web/js/toolbar/audits/duplicate-ids.js @@ -8,27 +8,17 @@ * Icon source: Tabler Icons (MIT) */ -import { applyHighlight, clearHighlight } from "./highlight.js"; - -/** @type {import('./index.js').AuditDefinition} */ -export default { - key: "duplicate-ids", - icon: '', - label: "Duplicate IDs", - description: - "Highlight elements sharing an ID with at least one other element", - - /** - * @param {object} context - Alpine toolbar component instance - * @param {boolean} active - true = activate, false = deactivate - */ - run(context, active) { - if (!active) { - clearHighlight(this.key); - context.setAuditDescription(this.key, this.description); - return; - } +import { createAudit } from "./createAudit.js"; +export default createAudit( + { + key: "duplicate-ids", + icon: '', + label: "Duplicate IDs", + description: + "Highlight elements sharing an ID with at least one other element", + }, + () => { /** @type {Map} */ const idMap = new Map(); @@ -42,25 +32,39 @@ export default { idMap.get(id).push(el); }); - /** @type {string[]} */ - const duplicateIdNames = []; + /** @type {Element[]} */ const duplicates = []; - idMap.forEach((els, id) => { + idMap.forEach((els) => { if (els.length > 1) { - duplicateIdNames.push(`#${id} (×${els.length})`); els.forEach((el) => duplicates.push(el)); } }); - if (duplicates.length > 0) { + return duplicates; + }, + (context, findings) => { + if (findings.length > 0) { + const idMap = new Map(); + document.querySelectorAll("[id]").forEach((el) => { + if (el.closest(".mageforge-toolbar")) return; + if (!idMap.has(el.id)) idMap.set(el.id, []); + idMap.get(el.id).push(el); + }); + const duplicateIdNames = []; + idMap.forEach((els, id) => { + if (els.length > 1) { + duplicateIdNames.push(`#${id} (×${els.length})`); + } + }); context.setAuditDescription( - this.key, + "duplicate-ids", `Duplicate: ${duplicateIdNames.join(", ")}`, ); } else { - context.setAuditDescription(this.key, this.description); + context.setAuditDescription( + "duplicate-ids", + "Highlight elements sharing an ID with at least one other element", + ); } - - applyHighlight(duplicates, this.key, context); }, -}; +); diff --git a/src/view/frontend/web/js/toolbar/audits/empty-interactive.js b/src/view/frontend/web/js/toolbar/audits/empty-interactive.js index 78cf143..e734623 100644 --- a/src/view/frontend/web/js/toolbar/audits/empty-interactive.js +++ b/src/view/frontend/web/js/toolbar/audits/empty-interactive.js @@ -5,68 +5,57 @@ * reader and keyboard users (WCAG 2.1 SC 4.1.2, 2.4.6). */ -import { applyHighlight, clearHighlight } from "./highlight.js"; - -/** @type {import('./index.js').AuditDefinition} */ -export default { - key: "empty-interactive", - icon: '', - label: "Empty Links & Buttons", - description: "Highlight links and buttons missing an accessible name", - - /** - * @param {object} context - Alpine toolbar component instance - * @param {boolean} active - true = activate, false = deactivate - */ - run(context, active) { - if (!active) { - clearHighlight(this.key); - return; - } - - const elements = Array.from( - document.querySelectorAll("a[href], button"), - ).filter((el) => { - // Visibility check - if (!el.offsetParent && getComputedStyle(el).position !== "fixed") - return false; - const style = getComputedStyle(el); - if ( - style.visibility === "hidden" || - style.display === "none" || - parseFloat(style.opacity) === 0 - ) - return false; - - // Accessible name sources - if (el.getAttribute("aria-label")?.trim()) return false; - if (el.getAttribute("title")?.trim()) return false; - if ( - el - .getAttribute("aria-labelledby") - ?.trim() - .split(/\s+/) - .some((id) => document.getElementById(id)?.textContent.trim()) - ) - return false; +import { createAudit } from "./createAudit.js"; + +export default createAudit( + { + key: "empty-interactive", + icon: '', + label: "Empty Links & Buttons", + description: "Highlight links and buttons missing an accessible name", + }, + () => { + return Array.from(document.querySelectorAll("a[href], button")).filter( + (el) => { + // Visibility check + if (!el.offsetParent && getComputedStyle(el).position !== "fixed") + return false; + const style = getComputedStyle(el); + if ( + style.visibility === "hidden" || + style.display === "none" || + parseFloat(style.opacity) === 0 + ) + return false; + + // Accessible name sources + if (el.getAttribute("aria-label")?.trim()) return false; + if (el.getAttribute("title")?.trim()) return false; + if ( + el + .getAttribute("aria-labelledby") + ?.trim() + .split(/\s+/) + .some((id) => document.getElementById(id)?.textContent.trim()) + ) + return false; - // Text content (excluding whitespace-only) - if (el.textContent.trim()) return false; + // Text content (excluding whitespace-only) + if (el.textContent.trim()) return false; - // Child with non-empty alt (trimmed) - if ( - Array.from(el.querySelectorAll("img[alt]")).some((img) => - img.getAttribute("alt")?.trim(), + // Child with non-empty alt (trimmed) + if ( + Array.from(el.querySelectorAll("img[alt]")).some((img) => + img.getAttribute("alt")?.trim(), + ) ) - ) - return false; - - // Child with a element - if (el.querySelector("svg title")?.textContent.trim()) return false; + return false; - return true; - }); + // Child with a element + if (el.querySelector("svg title")?.textContent.trim()) return false; - applyHighlight(elements, this.key, context); + return true; + }, + ); }, -}; +); diff --git a/src/view/frontend/web/js/toolbar/audits/highlight.js b/src/view/frontend/web/js/toolbar/audits/highlight.js index 9c47779..c9e3d68 100644 --- a/src/view/frontend/web/js/toolbar/audits/highlight.js +++ b/src/view/frontend/web/js/toolbar/audits/highlight.js @@ -123,6 +123,30 @@ export function clearHighlight(key) { }); } +/** + * Build a short, human-readable CSS selector for labelling a finding. + * + * @param {Element} el + * @returns {string} + */ +export function getReadableSelector(el) { + if (el.id) return `#${el.id}`; + const tag = el.tagName.toLowerCase(); + const classes = [...el.classList] + .filter((c) => !c.startsWith("mageforge")) + .slice(0, 2) + .join("."); + if (classes) return `${tag}.${classes}`; + const ariaLabel = el.getAttribute("aria-label"); + if (ariaLabel) return `${tag}[aria-label]`; + if (el.name) return `${tag}[name="${el.name}"]`; + if (tag === "img" && el.src) { + const base = el.src.split("/").pop().split("?")[0].slice(0, 24); + return `img/${base}`; + } + return tag; +} + /** * Highlights a set of elements by injecting a positioned overlay, scrolls to * the first result, and updates the counter badge on the toolbar menu item. @@ -134,16 +158,21 @@ export function clearHighlight(key) { * @param {object} [options={}] - Options * @param {'error'|'warning'} [options.severity='error'] - Visual severity level * @param {boolean} [options.skipBadge=false] - Skip badge + scroll update + * @param {boolean} [options.autoFindings=false] - Auto-generate findings list for "Affected Elements" panel + * @param {(el: Element, severity: string) => {selector?: string, action?: string}} [options.formatFinding] - Custom formatter for each finding row */ export function applyHighlight(elements, key, context, options = {}) { const severity = options.severity ?? "error"; const skipBadge = options.skipBadge ?? false; + const autoFindings = options.autoFindings ?? false; + const formatFinding = options.formatFinding; // Never flag elements that are part of the MageForge Toolbar itself elements = elements.filter((el) => !el.closest(".mageforge-toolbar")); if (elements.length === 0) { if (!skipBadge) context.setAuditCounterBadge(key, "0", "success"); + if (autoFindings) context.setAuditFindings(key, []); return; } const cls = `mageforge-audit-${key}`; @@ -165,4 +194,13 @@ export function applyHighlight(elements, key, context, options = {}) { } context.setAuditCounterBadge(key, `${elements.length}`, severity); } + + // Auto-generate findings for the "Affected Elements" panel + if (autoFindings) { + const findings = elements.map((el) => { + const base = { el, selector: getReadableSelector(el), severity }; + return formatFinding ? { ...base, ...formatFinding(el, severity) } : base; + }); + context.setAuditFindings(key, findings); + } } diff --git a/src/view/frontend/web/js/toolbar/audits/images-without-alt.js b/src/view/frontend/web/js/toolbar/audits/images-without-alt.js index f40732b..4399a14 100644 --- a/src/view/frontend/web/js/toolbar/audits/images-without-alt.js +++ b/src/view/frontend/web/js/toolbar/audits/images-without-alt.js @@ -2,26 +2,16 @@ * MageForge Toolbar Audit – Images without ALT */ -import { applyHighlight, clearHighlight } from "./highlight.js"; - -/** @type {import('./index.js').AuditDefinition} */ -export default { - key: "images-without-alt", - icon: '', - label: "Images without ALT", - description: "Highlight images without alt attributes", - - /** - * @param {object} context - Alpine toolbar component instance - * @param {boolean} active - true = activate, false = deactivate - */ - run(context, active) { - if (!active) { - clearHighlight(this.key); - context.setAuditFindings(this.key, []); - return; - } - +import { createAudit } from "./createAudit.js"; + +export default createAudit( + { + key: "images-without-alt", + icon: '', + label: "Images without ALT", + description: "Highlight images without alt attributes", + }, + () => { const visible = Array.from(document.querySelectorAll("img")).filter( (img) => { if (!img.offsetParent && getComputedStyle(img).position !== "fixed") @@ -45,34 +35,6 @@ export default { // Warnings: explicit alt="" – intentionally decorative, but flagged for review const warnings = visible.filter((img) => img.getAttribute("alt") === ""); - const total = errors.length + warnings.length; - if (total === 0) { - context.setAuditCounterBadge(this.key, "0", "success"); - context.setAuditFindings(this.key, []); - return; - } - - applyHighlight(errors, this.key, context, { skipBadge: true }); - applyHighlight(warnings, this.key, context, { - severity: "warning", - skipBadge: true, - }); - - // Populate the clickable findings list - context.setAuditFindings(this.key, [ - ...errors.map((el) => ({ el, severity: "error", action: "Add alt text" })), - ...warnings.map((el) => ({ el, severity: "warning", action: "Check alt text" })), - ]); - - // Scroll to first issue (errors take priority) - const first = errors[0] ?? warnings[0]; - if (first && !context._batchRunning) - first.scrollIntoView({ behavior: "smooth", block: "center" }); - - context.setAuditCounterBadge( - this.key, - `${total}`, - errors.length > 0 ? "error" : "warning", - ); + return { errors, warnings }; }, -}; +); diff --git a/src/view/frontend/web/js/toolbar/audits/images-without-dimensions.js b/src/view/frontend/web/js/toolbar/audits/images-without-dimensions.js index cf420d2..b79e0fb 100644 --- a/src/view/frontend/web/js/toolbar/audits/images-without-dimensions.js +++ b/src/view/frontend/web/js/toolbar/audits/images-without-dimensions.js @@ -4,40 +4,27 @@ * Images missing explicit width and height attributes cause Cumulative Layout Shift (CLS). */ -import { applyHighlight, clearHighlight } from "./highlight.js"; +import { createAudit } from "./createAudit.js"; -/** @type {import('./index.js').AuditDefinition} */ -export default { - key: "images-without-dimensions", - icon: '', - label: "Images without Dimensions", - description: "Highlight images missing width/height", - - /** - * @param {object} context - Alpine toolbar component instance - * @param {boolean} active - true = activate, false = deactivate - */ - run(context, active) { - if (!active) { - clearHighlight(this.key); - return; - } - - const images = Array.from(document.querySelectorAll("img")).filter( - (img) => { - if (!img.offsetParent && getComputedStyle(img).position !== "fixed") - return false; - const style = getComputedStyle(img); - if ( - style.visibility === "hidden" || - style.display === "none" || - parseFloat(style.opacity) === 0 - ) - return false; - return !img.hasAttribute("width") || !img.hasAttribute("height"); - }, - ); - - applyHighlight(images, this.key, context); +export default createAudit( + { + key: "images-without-dimensions", + icon: '', + label: "Images without Dimensions", + description: "Highlight images missing width/height", + }, + () => { + return Array.from(document.querySelectorAll("img")).filter((img) => { + if (!img.offsetParent && getComputedStyle(img).position !== "fixed") + return false; + const style = getComputedStyle(img); + if ( + style.visibility === "hidden" || + style.display === "none" || + parseFloat(style.opacity) === 0 + ) + return false; + return !img.hasAttribute("width") || !img.hasAttribute("height"); + }); }, -}; +); diff --git a/src/view/frontend/web/js/toolbar/audits/images-without-lazy-load.js b/src/view/frontend/web/js/toolbar/audits/images-without-lazy-load.js index b45940a..ccbb1b9 100644 --- a/src/view/frontend/web/js/toolbar/audits/images-without-lazy-load.js +++ b/src/view/frontend/web/js/toolbar/audits/images-without-lazy-load.js @@ -5,44 +5,31 @@ * wasting bandwidth and slowing initial page load. */ -import { applyHighlight, clearHighlight } from "./highlight.js"; - -/** @type {import('./index.js').AuditDefinition} */ -export default { - key: "images-without-lazy-load", - icon: '', - label: "Images without Lazy Load", - description: 'Highlight off-screen images missing loading="lazy"', - - /** - * @param {object} context - Alpine toolbar component instance - * @param {boolean} active - true = activate, false = deactivate - */ - run(context, active) { - if (!active) { - clearHighlight(this.key); - return; - } +import { createAudit } from "./createAudit.js"; +export default createAudit( + { + key: "images-without-lazy-load", + icon: '', + label: "Images without Lazy Load", + description: 'Highlight off-screen images missing loading="lazy"', + }, + () => { const viewportBottom = window.innerHeight; - const images = Array.from(document.querySelectorAll("img")).filter( - (img) => { - if (!img.offsetParent && getComputedStyle(img).position !== "fixed") - return false; - const style = getComputedStyle(img); - if ( - style.visibility === "hidden" || - style.display === "none" || - parseFloat(style.opacity) === 0 - ) - return false; - if (img.getAttribute("loading") === "lazy") return false; - const rect = img.getBoundingClientRect(); - return rect.top > viewportBottom; - }, - ); - - applyHighlight(images, this.key, context); + return Array.from(document.querySelectorAll("img")).filter((img) => { + if (!img.offsetParent && getComputedStyle(img).position !== "fixed") + return false; + const style = getComputedStyle(img); + if ( + style.visibility === "hidden" || + style.display === "none" || + parseFloat(style.opacity) === 0 + ) + return false; + if (img.getAttribute("loading") === "lazy") return false; + const rect = img.getBoundingClientRect(); + return rect.top > viewportBottom; + }); }, -}; +); diff --git a/src/view/frontend/web/js/toolbar/audits/inputs-without-label.js b/src/view/frontend/web/js/toolbar/audits/inputs-without-label.js index 9e026ca..b2f004d 100644 --- a/src/view/frontend/web/js/toolbar/audits/inputs-without-label.js +++ b/src/view/frontend/web/js/toolbar/audits/inputs-without-label.js @@ -5,26 +5,17 @@ * to screen reader users. */ -import { applyHighlight, clearHighlight } from "./highlight.js"; +import { createAudit } from "./createAudit.js"; -/** @type {import('./index.js').AuditDefinition} */ -export default { - key: "inputs-without-label", - icon: '', - label: "Inputs without Label", - description: "Highlight form inputs missing a label or aria-label", - - /** - * @param {object} context - Alpine toolbar component instance - * @param {boolean} active - true = activate, false = deactivate - */ - run(context, active) { - if (!active) { - clearHighlight(this.key); - return; - } - - const inputs = Array.from( +export default createAudit( + { + key: "inputs-without-label", + icon: '', + label: "Inputs without Label", + description: "Highlight form inputs missing a label or aria-label", + }, + () => { + return Array.from( document.querySelectorAll( 'input:not([type="hidden"]):not([type="submit"]):not([type="button"]):not([type="reset"]):not([type="image"]), select, textarea', ), @@ -57,7 +48,5 @@ export default { if (input.closest("label")) return false; return true; }); - - applyHighlight(inputs, this.key, context); }, -}; +); diff --git a/src/view/frontend/web/js/toolbar/audits/low-contrast-text.js b/src/view/frontend/web/js/toolbar/audits/low-contrast-text.js index c38dc07..a0e5630 100644 --- a/src/view/frontend/web/js/toolbar/audits/low-contrast-text.js +++ b/src/view/frontend/web/js/toolbar/audits/low-contrast-text.js @@ -6,7 +6,7 @@ * - 3:1 for large text (>=18pt or >=14pt bold) */ -import { applyHighlight, clearHighlight } from "./highlight.js"; +import { createAudit } from "./createAudit.js"; const _colorCanvas = document.createElement("canvas"); _colorCanvas.width = _colorCanvas.height = 1; @@ -156,22 +156,14 @@ function isLargeText(el) { } /** @type {import('./index.js').AuditDefinition} */ -export default { - key: "low-contrast-text", - icon: '', - label: "Low Contrast Text", - description: "Highlight text failing WCAG AA contrast", - - /** - * @param {object} context - Alpine toolbar component instance - * @param {boolean} active - true = activate, false = deactivate - */ - run(context, active) { - if (!active) { - clearHighlight(this.key); - return; - } - +export default createAudit( + { + key: "low-contrast-text", + icon: '', + label: "Low Contrast Text", + description: "Highlight text failing WCAG AA contrast", + }, + () => { const candidates = Array.from( document.querySelectorAll( "p, a, h1, h2, h3, h4, h5, h6, li, td, th, label, button", @@ -184,7 +176,7 @@ export default { return hasDirectText(el); }); - const failing = candidates.filter((el) => { + return candidates.filter((el) => { const style = getComputedStyle(el); const fg = parseColor(style.color); if (!fg || fg[3] === 0) return false; @@ -194,7 +186,5 @@ export default { const threshold = isLargeText(el) ? 3 : 4.5; return ratio < threshold; }); - - applyHighlight(failing, this.key, context); }, -}; +); diff --git a/src/view/frontend/web/js/toolbar/audits/multiple-h1.js b/src/view/frontend/web/js/toolbar/audits/multiple-h1.js index c5df4a0..c22d9ed 100644 --- a/src/view/frontend/web/js/toolbar/audits/multiple-h1.js +++ b/src/view/frontend/web/js/toolbar/audits/multiple-h1.js @@ -5,25 +5,16 @@ * readers and harm SEO by diluting the primary heading signal. */ -import { applyHighlight, clearHighlight } from "./highlight.js"; - -/** @type {import('./index.js').AuditDefinition} */ -export default { - key: "multiple-h1", - icon: '', - label: "Multiple H1", - description: "Highlight pages with more than one H1 heading", - - /** - * @param {object} context - Alpine toolbar component instance - * @param {boolean} active - true = activate, false = deactivate - */ - run(context, active) { - if (!active) { - clearHighlight(this.key); - return; - } +import { createAudit } from "./createAudit.js"; +export default createAudit( + { + key: "multiple-h1", + icon: '', + label: "Multiple H1", + description: "Highlight pages with more than one H1 heading", + }, + () => { const h1s = Array.from(document.querySelectorAll("h1")).filter((el) => { if (!el.offsetParent && getComputedStyle(el).position !== "fixed") return false; @@ -37,11 +28,7 @@ export default { return true; }); - if (h1s.length <= 1) { - context.setAuditCounterBadge(this.key, "0", "success"); - return; - } - - applyHighlight(h1s, this.key, context); + // Only flag when there's more than one H1 + return h1s.length > 1 ? h1s : []; }, -}; +); diff --git a/src/view/frontend/web/js/toolbar/audits/small-touch-targets.js b/src/view/frontend/web/js/toolbar/audits/small-touch-targets.js index b404cea..7183a12 100644 --- a/src/view/frontend/web/js/toolbar/audits/small-touch-targets.js +++ b/src/view/frontend/web/js/toolbar/audits/small-touch-targets.js @@ -7,27 +7,18 @@ * Icon source: Tabler Icons (MIT) */ -import { applyHighlight, clearHighlight } from "./highlight.js"; +import { createAudit } from "./createAudit.js"; const MIN_SIZE = 24; -/** @type {import('./index.js').AuditDefinition} */ -export default { - key: "small-touch-targets", - icon: '', - label: "Small Touch Targets", - description: `Highlight interactive elements smaller than ${MIN_SIZE}×${MIN_SIZE} px (WCAG 2.5.8 AA)`, - - /** - * @param {object} context - Alpine toolbar component instance - * @param {boolean} active - true = activate, false = deactivate - */ - run(context, active) { - if (!active) { - clearHighlight(this.key); - return; - } - +export default createAudit( + { + key: "small-touch-targets", + icon: '', + label: "Small Touch Targets", + description: `Highlight interactive elements smaller than ${MIN_SIZE}×${MIN_SIZE} px (WCAG 2.5.8 AA)`, + }, + () => { const selector = [ 'a[href]:not([aria-disabled="true"])', 'button:not([disabled]):not([aria-disabled="true"])', @@ -41,24 +32,20 @@ export default { 'textarea:not([disabled]):not([aria-disabled="true"])', ].join(", "); - const elements = Array.from(document.querySelectorAll(selector)).filter( - (el) => { - if (el.matches('[disabled], [aria-disabled="true"]')) return false; - if (!el.offsetParent && getComputedStyle(el).position !== "fixed") - return false; - const style = getComputedStyle(el); - if ( - style.visibility === "hidden" || - style.display === "none" || - parseFloat(style.opacity) === 0 - ) - return false; - - const rect = el.getBoundingClientRect(); - return rect.width < MIN_SIZE || rect.height < MIN_SIZE; - }, - ); - - applyHighlight(elements, this.key, context); + return Array.from(document.querySelectorAll(selector)).filter((el) => { + if (el.matches('[disabled], [aria-disabled="true"]')) return false; + if (!el.offsetParent && getComputedStyle(el).position !== "fixed") + return false; + const style = getComputedStyle(el); + if ( + style.visibility === "hidden" || + style.display === "none" || + parseFloat(style.opacity) === 0 + ) + return false; + + const rect = el.getBoundingClientRect(); + return rect.width < MIN_SIZE || rect.height < MIN_SIZE; + }); }, -}; +); diff --git a/src/view/frontend/web/js/toolbar/audits/unsafe-blank-target.js b/src/view/frontend/web/js/toolbar/audits/unsafe-blank-target.js index 794ed75..b8ef9d0 100644 --- a/src/view/frontend/web/js/toolbar/audits/unsafe-blank-target.js +++ b/src/view/frontend/web/js/toolbar/audits/unsafe-blank-target.js @@ -9,43 +9,32 @@ * Icon source: Tabler Icons (MIT) */ -import { applyHighlight, clearHighlight } from "./highlight.js"; +import { createAudit } from "./createAudit.js"; -/** @type {import('./index.js').AuditDefinition} */ -export default { - key: "unsafe-blank-target", - icon: '', - label: "Unsafe Blank Target", - description: - 'Highlight target="_blank" links with neither rel="noopener" nor rel="noreferrer"', - - /** - * @param {object} context - Alpine toolbar component instance - * @param {boolean} active - true = activate, false = deactivate - */ - run(context, active) { - if (!active) { - clearHighlight(this.key); - return; - } - - const elements = Array.from( - document.querySelectorAll('a[target="_blank"]'), - ).filter((el) => { - if (!el.offsetParent && getComputedStyle(el).position !== "fixed") - return false; - const style = getComputedStyle(el); - if ( - style.visibility === "hidden" || - style.display === "none" || - parseFloat(style.opacity) === 0 - ) - return false; - - const rel = (el.getAttribute("rel") || "").toLowerCase().split(/\s+/); - return !rel.includes("noopener") && !rel.includes("noreferrer"); - }); +export default createAudit( + { + key: "unsafe-blank-target", + icon: '', + label: "Unsafe Blank Target", + description: + 'Highlight target="_blank" links with neither rel="noopener" nor rel="noreferrer"', + }, + () => { + return Array.from(document.querySelectorAll('a[target="_blank"]')).filter( + (el) => { + if (!el.offsetParent && getComputedStyle(el).position !== "fixed") + return false; + const style = getComputedStyle(el); + if ( + style.visibility === "hidden" || + style.display === "none" || + parseFloat(style.opacity) === 0 + ) + return false; - applyHighlight(elements, this.key, context); + const rel = (el.getAttribute("rel") || "").toLowerCase().split(/\s+/); + return !rel.includes("noopener") && !rel.includes("noreferrer"); + }, + ); }, -}; +); diff --git a/src/view/frontend/web/js/toolbar/ui.js b/src/view/frontend/web/js/toolbar/ui.js index 895663f..8763f06 100644 --- a/src/view/frontend/web/js/toolbar/ui.js +++ b/src/view/frontend/web/js/toolbar/ui.js @@ -31,12 +31,15 @@ const LOGO_SVG_PATH = "M176 0L0 101.614V297L176 398.614L352 297V101.614L176 0ZM39 275.5V124L76.2391 101.614L101.5 162L126.5 73.4393L164.5 51.5V346.939L126.5 325V188L108.5 239H95L76.2391 188V297L39 275.5ZM187.5 346.939V51.5L313 124V170H275.5V146.368L225.5 117.5V188H280V226.5H225.5V325L187.5 346.939Z"; -const ICON_HOME = ''; +const ICON_HOME = + ''; const GROUP_ICONS = { - "wcag": '', - "html-quality": '', - "performance": '', + wcag: '', + "html-quality": + '', + performance: + '', }; // ── Module-level helpers ─────────────────────────────────────────────────── @@ -72,7 +75,6 @@ function getReadableSelector(el) { // ── Exported mixin ───────────────────────────────────────────────────────── export const uiMethods = { - // ──────────────────────────────────────────────────────────────────────── // Entry point // ──────────────────────────────────────────────────────────────────────── @@ -85,10 +87,16 @@ export const uiMethods = { this.container.className = "mageforge-toolbar"; if (this.$el?.hasAttribute("data-theme")) { - this.container.setAttribute("data-theme", this.$el.getAttribute("data-theme")); + this.container.setAttribute( + "data-theme", + this.$el.getAttribute("data-theme"), + ); } if (this.$el?.hasAttribute("data-position")) { - this.container.setAttribute("data-position", this.$el.getAttribute("data-position")); + this.container.setAttribute( + "data-position", + this.$el.getAttribute("data-position"), + ); } if (this.$el?.getAttribute("data-show-labels") === "0") { this.container.classList.add("mageforge-toolbar--no-labels"); @@ -182,7 +190,9 @@ export const uiMethods = { nav.appendChild(this._buildNavTab("home", ICON_HOME, "Home", true)); this.getAuditGroups().forEach((group) => { - nav.appendChild(this._buildNavTab(group.key, GROUP_ICONS[group.key] ?? "", group.label)); + nav.appendChild( + this._buildNavTab(group.key, GROUP_ICONS[group.key] ?? "", group.label), + ); }); return nav; @@ -211,9 +221,16 @@ export const uiMethods = { ${label.split(" ")[0]} `; - btn.onclick = (e) => { e.stopPropagation(); this.switchTab(key); }; + btn.onclick = (e) => { + e.stopPropagation(); + this.switchTab(key); + }; btn.onkeydown = (e) => { - if (e.key === "Enter" || e.key === " ") { e.preventDefault(); e.stopPropagation(); this.switchTab(key); } + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + e.stopPropagation(); + this.switchTab(key); + } }; return btn; }, @@ -231,7 +248,8 @@ export const uiMethods = { const wrapper = document.createElement("div"); wrapper.className = "mageforge-toolbar-tab-content"; - const showHealthScore = this.$el?.getAttribute("data-show-health-score") !== "0"; + const showHealthScore = + this.$el?.getAttribute("data-show-health-score") !== "0"; const grouped = {}; const ungrouped = []; @@ -261,7 +279,10 @@ export const uiMethods = { items.forEach((audit) => { body.appendChild( this.createMenuItem( - audit.key, audit.icon, audit.label, audit.description, + audit.key, + audit.icon, + audit.label, + audit.description, () => this.runAudit(audit.key), group.key, ), @@ -278,7 +299,10 @@ export const uiMethods = { ungrouped.forEach((audit) => { panel.appendChild( this.createMenuItem( - audit.key, audit.icon, audit.label, audit.description, + audit.key, + audit.icon, + audit.label, + audit.description, () => this.runAudit(audit.key), ), ); @@ -340,15 +364,15 @@ export const uiMethods = {
@@ -378,20 +402,20 @@ export const uiMethods = {
@@ -433,15 +457,29 @@ export const uiMethods = { this.runAllButton.setAttribute("tabindex", "0"); this.runAllButton.className = "mageforge-toolbar-menu-run-all"; this.runAllButton.innerHTML = ` - + RUN ALL TESTS `; - this.runAllButton.onclick = (e) => { e.stopPropagation(); this.runAllAuditsForScore(); }; + this.runAllButton.onclick = (e) => { + e.stopPropagation(); + this.runAllAuditsForScore(); + }; this.runAllButton.onkeydown = (e) => { - if (e.key === "Enter") { e.preventDefault(); e.stopPropagation(); this.runAllAuditsForScore(); } - if (e.key === " ") { e.preventDefault(); } + 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(); + } }; - this.runAllButton.onkeyup = (e) => { if (e.key === " ") { e.stopPropagation(); this.runAllAuditsForScore(); } }; btnRow.appendChild(this.runAllButton); this.resetButton = document.createElement("div"); @@ -449,14 +487,32 @@ export const uiMethods = { 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"); - this.resetButton.innerHTML = ''; - this.resetButton.onclick = (e) => { e.stopPropagation(); this.resetScore(); }; + this.resetButton.setAttribute( + "aria-label", + "Reset score and deactivate all audits", + ); + this.resetButton.innerHTML = + ''; + this.resetButton.onclick = (e) => { + e.stopPropagation(); + this.resetScore(); + }; this.resetButton.onkeydown = (e) => { - if (e.key === "Enter") { e.preventDefault(); e.stopPropagation(); this.resetScore(); } - if (e.key === " ") { e.preventDefault(); } + 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(); + } }; - this.resetButton.onkeyup = (e) => { if (e.key === " ") { e.stopPropagation(); this.resetScore(); } }; btnRow.appendChild(this.resetButton); footer.appendChild(btnRow); @@ -464,7 +520,8 @@ export const uiMethods = { // ── Credit line ───────────────────────────────────────────────────── const credit = document.createElement("div"); credit.className = "mageforge-toolbar-menu-credit"; - credit.innerHTML = 'Built with \u2764 by MageForge'; + credit.innerHTML = + 'Built with \u2764 by MageForge'; footer.appendChild(credit); return footer; @@ -491,12 +548,27 @@ export const uiMethods = { MageForge `; - btn.onclick = (e) => { e.preventDefault(); e.stopPropagation(); this.toggleMenu(); }; + btn.onclick = (e) => { + e.preventDefault(); + e.stopPropagation(); + this.toggleMenu(); + }; btn.onkeydown = (e) => { - if (e.key === "Enter") { e.preventDefault(); e.stopPropagation(); this.toggleMenu(); } - if (e.key === " ") { e.preventDefault(); } + if (e.key === "Enter") { + e.preventDefault(); + e.stopPropagation(); + this.toggleMenu(); + } + if (e.key === " ") { + e.preventDefault(); + } + }; + btn.onkeyup = (e) => { + if (e.key === " ") { + e.stopPropagation(); + this.toggleMenu(); + } }; - btn.onkeyup = (e) => { if (e.key === " ") { e.stopPropagation(); this.toggleMenu(); } }; return btn; }, @@ -520,11 +592,15 @@ export const uiMethods = { btn.setAttribute("tabindex", active ? "0" : "-1"); }); - this.menu.querySelectorAll(".mageforge-toolbar-tab-panel").forEach((panel) => { - const active = panel.dataset.panel === key; - panel.classList.toggle("mageforge-tab-panel-active", active); - active ? panel.removeAttribute("hidden") : panel.setAttribute("hidden", ""); - }); + this.menu + .querySelectorAll(".mageforge-toolbar-tab-panel") + .forEach((panel) => { + const active = panel.dataset.panel === key; + panel.classList.toggle("mageforge-tab-panel-active", active); + active + ? panel.removeAttribute("hidden") + : panel.setAttribute("hidden", ""); + }); }, // ──────────────────────────────────────────────────────────────────────── @@ -569,12 +645,27 @@ export const uiMethods = { findings.addEventListener("click", (e) => e.stopPropagation()); item.appendChild(findings); - item.onclick = (e) => { e.preventDefault(); e.stopPropagation(); callback(); }; + item.onclick = (e) => { + e.preventDefault(); + e.stopPropagation(); + callback(); + }; item.onkeydown = (e) => { - if (e.key === "Enter") { e.preventDefault(); e.stopPropagation(); callback(); } - if (e.key === " ") { e.preventDefault(); } + if (e.key === "Enter") { + e.preventDefault(); + e.stopPropagation(); + callback(); + } + if (e.key === " ") { + e.preventDefault(); + } + }; + item.onkeyup = (e) => { + if (e.key === " ") { + e.stopPropagation(); + callback(); + } }; - item.onkeyup = (e) => { if (e.key === " ") { e.stopPropagation(); callback(); } }; return item; }, @@ -628,7 +719,7 @@ export const uiMethods = { row.innerHTML = ` ${selectorStr} - ${action ? `${action}` : ""} + Show Element `; row.addEventListener("click", (e) => { e.stopPropagation(); @@ -658,9 +749,15 @@ export const uiMethods = { item.setAttribute("aria-pressed", String(active)); if (!active) { - item.classList.remove("mageforge-active--error", "mageforge-active--warning"); + item.classList.remove( + "mageforge-active--error", + "mageforge-active--warning", + ); const status = item.querySelector(".mageforge-toolbar-menu-status"); - if (status) { status.textContent = ""; status.className = "mageforge-toolbar-menu-status"; } + if (status) { + status.textContent = ""; + status.className = "mageforge-toolbar-menu-status"; + } this.setAuditFindings(key, []); } @@ -681,26 +778,41 @@ export const uiMethods = { */ updateHealthScore(score) { if (!this.menu) return; - const ARC_LENGTH = 157.08; + const ARC_LENGTH = 157.08; const CIRCUMFERENCE = 113.1; // Half-arc gauge in the Home panel - const progress = this.menu.querySelector(".mageforge-health-gauge-progress"); - const needle = this.menu.querySelector(".mageforge-health-gauge-needle"); - if (progress) progress.setAttribute("stroke-dasharray", `${((score / 100) * ARC_LENGTH).toFixed(2)} ${ARC_LENGTH}`); + const progress = this.menu.querySelector( + ".mageforge-health-gauge-progress", + ); + const needle = this.menu.querySelector(".mageforge-health-gauge-needle"); + if (progress) + progress.setAttribute( + "stroke-dasharray", + `${((score / 100) * ARC_LENGTH).toFixed(2)} ${ARC_LENGTH}`, + ); if (needle) { const rad = (1 - score / 100) * Math.PI; needle.setAttribute("x2", (60 + 45 * Math.cos(rad)).toFixed(1)); needle.setAttribute("y2", (65 - 45 * Math.sin(rad)).toFixed(1)); needle.setAttribute("opacity", "1"); } - this.menu.querySelectorAll(".mageforge-toolbar-health-score-number").forEach((el) => { el.textContent = score; }); + this.menu + .querySelectorAll(".mageforge-toolbar-health-score-number") + .forEach((el) => { + el.textContent = score; + }); // Circular rings in audit panel headers this.menu.querySelectorAll(".mageforge-score-ring").forEach((ring) => { - ring.setAttribute("stroke-dasharray", `${((score / 100) * CIRCUMFERENCE).toFixed(2)} ${CIRCUMFERENCE}`); + ring.setAttribute( + "stroke-dasharray", + `${((score / 100) * CIRCUMFERENCE).toFixed(2)} ${CIRCUMFERENCE}`, + ); + }); + this.menu.querySelectorAll(".mageforge-score-number").forEach((el) => { + el.textContent = score; }); - this.menu.querySelectorAll(".mageforge-score-number").forEach((el) => { el.textContent = score; }); }, /** @@ -709,23 +821,35 @@ export const uiMethods = { resetScore() { this.deactivateAllAudits(); if (!this.menu) return; - const ARC_LENGTH = 157.08; + const ARC_LENGTH = 157.08; const CIRCUMFERENCE = 113.1; - const progress = this.menu.querySelector(".mageforge-health-gauge-progress"); - const needle = this.menu.querySelector(".mageforge-health-gauge-needle"); + const progress = this.menu.querySelector( + ".mageforge-health-gauge-progress", + ); + const needle = this.menu.querySelector(".mageforge-health-gauge-needle"); if (progress) progress.setAttribute("stroke-dasharray", `0 ${ARC_LENGTH}`); - if (needle) needle.setAttribute("opacity", "0"); - this.menu.querySelectorAll(".mageforge-toolbar-health-score-number").forEach((el) => { el.textContent = "--"; }); - this.menu.querySelectorAll(".mageforge-score-ring").forEach((ring) => { ring.setAttribute("stroke-dasharray", `0 ${CIRCUMFERENCE}`); }); - this.menu.querySelectorAll(".mageforge-score-number").forEach((el) => { el.textContent = "--"; }); + if (needle) needle.setAttribute("opacity", "0"); + this.menu + .querySelectorAll(".mageforge-toolbar-health-score-number") + .forEach((el) => { + el.textContent = "--"; + }); + this.menu.querySelectorAll(".mageforge-score-ring").forEach((ring) => { + ring.setAttribute("stroke-dasharray", `0 ${CIRCUMFERENCE}`); + }); + this.menu.querySelectorAll(".mageforge-score-number").forEach((el) => { + el.textContent = "--"; + }); }, // ──────────────────────────────────────────────────────────────────────── // Menu open / close // ──────────────────────────────────────────────────────────────────────── - toggleMenu() { this.menuOpen ? this.closeMenu() : this.openMenu(); }, + toggleMenu() { + this.menuOpen ? this.closeMenu() : this.openMenu(); + }, openMenu() { this.menuOpen = true; @@ -746,12 +870,13 @@ export const uiMethods = { document.removeEventListener("click", this._outsideClickHandler); this._outsideClickHandler = null; } - if (this.container?.parentNode) this.container.parentNode.removeChild(this.container); - this.container = null; - this.menu = null; + if (this.container?.parentNode) + this.container.parentNode.removeChild(this.container); + this.container = null; + this.menu = null; this.burgerButton = null; this.runAllButton = null; - this.resetButton = null; - this.menuOpen = false; + this.resetButton = null; + this.menuOpen = false; }, }; From f13ace37cd4c32696b363f215287b498d74f83cc Mon Sep 17 00:00:00 2001 From: Mathias Elle Date: Wed, 17 Jun 2026 01:11:10 +0200 Subject: [PATCH 05/48] feat: Add group-specific audit buttons and health score functionality in toolbar --- .../frontend/web/css/toolbar/_buttons.css | 161 +++++++++++++ src/view/frontend/web/css/toolbar/_menu.css | 5 +- src/view/frontend/web/js/toolbar/audits.js | 89 ++++++++ .../frontend/web/js/toolbar/audits/index.js | 2 +- src/view/frontend/web/js/toolbar/ui.js | 216 ++++++++++++------ 5 files changed, 395 insertions(+), 78 deletions(-) diff --git a/src/view/frontend/web/css/toolbar/_buttons.css b/src/view/frontend/web/css/toolbar/_buttons.css index 9c1f80b..ecb6706 100644 --- a/src/view/frontend/web/css/toolbar/_buttons.css +++ b/src/view/frontend/web/css/toolbar/_buttons.css @@ -82,3 +82,164 @@ background: var(--mageforge-surface-glass-hover); border-color: var(--mageforge-border-glass); } + +/* ── Home panel: Check Health Score button ─────────────────────────── */ + +.mageforge-home-check-btn { + flex: 1; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 8px; + width: 80%; + padding: 12px 20px; + background: linear-gradient( + 90deg, + var(--mageforge-color-blue) 0%, + var(--mageforge-color-pink) 100% + ); + border: none; + border-radius: 8px; + cursor: pointer; + color: var(--mageforge-color-white); + font-family: var(--mageforge-font-family); + font-size: 14px; + font-weight: 700; + letter-spacing: 0.06em; + text-transform: uppercase; + transition: + opacity 0.15s ease, + transform 0.15s ease, + box-shadow 0.15s ease; + box-shadow: 0 4px 14px rgba(59, 130, 246, 0.3); +} + +.mageforge-home-check-btn:hover { + opacity: 0.9; + transform: translateY(-1px); + box-shadow: 0 6px 18px rgba(59, 130, 246, 0.4); +} + +.mageforge-home-check-btn:active { + transform: translateY(0); +} + +.mageforge-home-check-btn:disabled, +.mageforge-home-check-btn.mageforge-running { + opacity: 0.6; + cursor: not-allowed; + transform: none; +} + +/* ── Home panel: Check + Reset button row ──────────────────────────── */ + +.mageforge-home-btn-row { + display: flex; + align-items: center; + gap: 8px; + width: 100%; + padding: 16px; +} + +.mageforge-home-reset-btn { + display: flex; + align-items: center; + justify-content: center; + width: 48px; + height: 48px; + background: none; + border: 1px solid var(--mageforge-border-color); + border-radius: 8px; + cursor: pointer; + color: var(--mageforge-color-slate-400); + transition: + color 0.15s ease, + background 0.15s ease, + border-color 0.15s ease; +} + +.mageforge-home-reset-btn:hover { + color: var(--mageforge-color-white); + background: var(--mageforge-surface-glass-hover); + border-color: var(--mageforge-border-glass); +} + +/* ── Group panels: Run [Group] Checks button ───────────────────────── */ + +.mageforge-group-run-btn { + flex: 1; + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + width: 100%; + padding: 10px 16px; + background: linear-gradient( + 90deg, + var(--mageforge-color-blue) 0%, + var(--mageforge-color-pink) 100% + ); + border: none; + border-radius: 7px; + cursor: pointer; + color: var(--mageforge-color-white); + font-family: var(--mageforge-font-family); + font-size: 13px; + font-weight: 700; + letter-spacing: 0.06em; + text-transform: uppercase; + transition: + opacity 0.15s ease, + transform 0.15s ease, + box-shadow 0.15s ease; + box-shadow: 0 3px 10px rgba(59, 130, 246, 0.25); +} + +.mageforge-group-run-btn:hover { + opacity: 0.9; + transform: translateY(-1px); + box-shadow: 0 5px 14px rgba(59, 130, 246, 0.35); +} + +.mageforge-group-run-btn:active { + transform: translateY(0); +} + +.mageforge-group-run-btn:disabled, +.mageforge-group-run-btn.mageforge-running { + opacity: 0.6; + cursor: not-allowed; + transform: none; +} + +/* ── Group panels: Run + Reset button row ──────────────────────────── */ + +.mageforge-group-btn-row { + display: flex; + align-items: center; + gap: 8px; + width: 100%; + margin-bottom: 16px; +} +.mageforge-group-reset-btn { + display: flex; + align-items: center; + justify-content: center; + width: 40px; + height: 40px; + background: none; + border: 1px solid var(--mageforge-border-color); + border-radius: 8px; + cursor: pointer; + color: var(--mageforge-color-slate-400); + transition: + color 0.15s ease, + background 0.15s ease, + border-color 0.15s ease; +} + +.mageforge-group-reset-btn:hover { + color: var(--mageforge-color-white); + background: var(--mageforge-surface-glass-hover); + border-color: var(--mageforge-border-glass); +} diff --git a/src/view/frontend/web/css/toolbar/_menu.css b/src/view/frontend/web/css/toolbar/_menu.css index dd9e2b2..92b6de8 100644 --- a/src/view/frontend/web/css/toolbar/_menu.css +++ b/src/view/frontend/web/css/toolbar/_menu.css @@ -85,10 +85,7 @@ font-size: 20px; font-weight: 700; padding-bottom: 2px; - color: transparent; - background-image: var(--gradient-brand); - background-clip: text; - -webkit-background-clip: text; + color: var(--mageforge-color-white); display: block; } diff --git a/src/view/frontend/web/js/toolbar/audits.js b/src/view/frontend/web/js/toolbar/audits.js index 3355a88..6d881a1 100644 --- a/src/view/frontend/web/js/toolbar/audits.js +++ b/src/view/frontend/web/js/toolbar/audits.js @@ -98,6 +98,88 @@ export const auditMethods = { } }, + /** + * Run all audits for a specific group, wait for the DOM to settle, then + * compute and display a score (0–100) in that panel's ring. + */ + async runGroupAuditsForScore(groupKey) { + const btn = this[`runGroupButton-${groupKey}`]; + if (!btn) return; + btn.disabled = true; + btn.classList.add("mageforge-running"); + + try { + this._batchRunning = true; + const groupAudits = audits.filter((a) => a.group === groupKey); + + // Deactivate existing audits in this group + groupAudits.forEach((audit) => { + if (this.activeAudits.has(audit.key)) { + this.activeAudits.delete(audit.key); + audit.run(this, false); + } + }); + + // Run all audits in the group + groupAudits.forEach((audit) => { + if (!this.activeAudits.has(audit.key)) { + this.runAudit(audit.key); + } + }); + + // Allow async DOM mutations to settle + await new Promise((resolve) => setTimeout(resolve, 200)); + + let totalPoints = 0; + let maxPoints = 0; + groupAudits.forEach((audit) => { + const item = this.menu?.querySelector( + `[data-audit-key="${audit.key}"]`, + ); + if (!item) return; + maxPoints += 100; + const status = item.querySelector(".mageforge-toolbar-menu-status"); + if (!status || !status.textContent.trim()) { + totalPoints += 100; + } else if ( + status.classList.contains("mageforge-toolbar-menu-status--success") + ) { + totalPoints += 100; + } else if ( + status.classList.contains("mageforge-toolbar-menu-status--warning") + ) { + totalPoints += 50; + } + }); + + const score = + maxPoints > 0 ? Math.round((totalPoints / maxPoints) * 100) : 100; + this.updateGroupScore(groupKey, score); + } finally { + this._batchRunning = false; + btn.disabled = false; + btn.classList.remove("mageforge-running"); + } + }, + + /** + * Reset all audits for a specific group (deactivate + hide score). + */ + resetGroupAudits(groupKey) { + const groupAudits = audits.filter((a) => a.group === groupKey); + groupAudits.forEach((audit) => { + if (this.activeAudits.has(audit.key)) { + this.activeAudits.delete(audit.key); + audit.run(this, false); + this.setAuditCounterBadge(audit.key, "", "success"); + this.setAuditActive(audit.key, false); + } + }); + + // Reset score ring + this.updateGroupScore(groupKey, 0); + }, + /** * Deactivates all currently active audits (called when closing the toolbar). */ @@ -110,6 +192,13 @@ export const auditMethods = { this.setAuditCounterBadge(key, "", "success"); this.setAuditActive(key, false); }); + + // Hide all group reset buttons + this.getAuditGroups().forEach((group) => { + const resetBtn = this[`groupResetButton-${group.key}`]; + if (resetBtn) resetBtn.style.display = "none"; + }); + this.updateToggleAllButton(); }, diff --git a/src/view/frontend/web/js/toolbar/audits/index.js b/src/view/frontend/web/js/toolbar/audits/index.js index 12274c5..777ef97 100644 --- a/src/view/frontend/web/js/toolbar/audits/index.js +++ b/src/view/frontend/web/js/toolbar/audits/index.js @@ -37,8 +37,8 @@ import unsafeBlankTarget from "./unsafe-blank-target.js"; /** @type {AuditGroup[]} */ export const auditGroups = [ - { key: "wcag", label: "WCAG Checks" }, { key: "html-quality", label: "HTML Quality" }, + { key: "wcag", label: "Accessibility" }, { key: "performance", label: "Performance" }, ]; diff --git a/src/view/frontend/web/js/toolbar/ui.js b/src/view/frontend/web/js/toolbar/ui.js index 8763f06..ee27f5c 100644 --- a/src/view/frontend/web/js/toolbar/ui.js +++ b/src/view/frontend/web/js/toolbar/ui.js @@ -276,6 +276,61 @@ export const uiMethods = { const body = document.createElement("div"); body.className = "mageforge-tab-panel-body"; + + // Group-specific run button row at the top of the panel body + const groupBtnRow = document.createElement("div"); + groupBtnRow.className = "mageforge-group-btn-row"; + + const groupBtn = document.createElement("button"); + groupBtn.type = "button"; + groupBtn.className = "mageforge-group-run-btn"; + groupBtn.dataset.group = group.key; + this[`runGroupButton-${group.key}`] = groupBtn; + const groupLabel = this.getAuditGroups().find((g) => g.key === group.key)?.label ?? group.key; + groupBtn.innerHTML = ` + + Run ${groupLabel} Checks + `; + groupBtn.onclick = (e) => { + e.stopPropagation(); + this.runGroupAuditsForScore(group.key); + }; + groupBtnRow.appendChild(groupBtn); + + // Reset button (hidden by default, shown after run) + const groupResetBtn = document.createElement("div"); + groupResetBtn.setAttribute("role", "button"); + groupResetBtn.setAttribute("tabindex", "0"); + groupResetBtn.className = "mageforge-group-reset-btn"; + groupResetBtn.setAttribute("aria-label", `Reset ${groupLabel} audits`); + groupResetBtn.title = `Reset ${groupLabel} audits`; + groupResetBtn.innerHTML = + ''; + groupResetBtn.onclick = (e) => { + e.stopPropagation(); + this.resetGroupAudits(group.key); + }; + groupResetBtn.onkeydown = (e) => { + if (e.key === "Enter") { + e.preventDefault(); + e.stopPropagation(); + this.resetGroupAudits(group.key); + } + if (e.key === " ") e.preventDefault(); + }; + groupResetBtn.onkeyup = (e) => { + if (e.key === " ") { + e.stopPropagation(); + this.resetGroupAudits(group.key); + } + }; + groupBtnRow.appendChild(groupResetBtn); + + // Store reference for showing/hiding + this[`groupResetButton-${group.key}`] = groupResetBtn; + + body.appendChild(groupBtnRow); + items.forEach((audit) => { body.appendChild( this.createMenuItem( @@ -385,7 +440,7 @@ export const uiMethods = { }, /** - * Home panel: half-arc gauge overview + intro hint. + * Home panel: half-arc gauge overview + Check Health Score button. * * @param {boolean} showHealthScore * @returns {HTMLDivElement} @@ -425,8 +480,67 @@ export const uiMethods = {
Overall Health Score
-

Select a category on the left or click Run All Tests below.

`; + + // Check Health Score button + Reset side by side + const btnRow = document.createElement("div"); + btnRow.className = "mageforge-home-btn-row"; + + this.runAllButton = document.createElement("button"); + this.runAllButton.type = "button"; + this.runAllButton.className = "mageforge-home-check-btn"; + this.runAllButton.innerHTML = ` + + Check Health Score + `; + this.runAllButton.onclick = (e) => { + e.stopPropagation(); + this.runAllAuditsForScore(); + }; + btnRow.appendChild(this.runAllButton); + + // Reset button next to Check Health Score + this.resetButton = document.createElement("div"); + this.resetButton.setAttribute("role", "button"); + this.resetButton.setAttribute("tabindex", "0"); + this.resetButton.className = "mageforge-home-reset-btn"; + this.resetButton.title = "Reset score and deactivate all audits"; + this.resetButton.setAttribute( + "aria-label", + "Reset score and deactivate all audits", + ); + this.resetButton.innerHTML = + ''; + this.resetButton.onclick = (e) => { + e.stopPropagation(); + this.resetScore(); + }; + this.resetButton.onkeydown = (e) => { + 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(); + } + }; + btnRow.appendChild(this.resetButton); + + panel.appendChild(btnRow); + + panel.appendChild( + Object.assign(document.createElement("p"), { + className: "mageforge-home-hint", + textContent: "Select a category on the left for detailed checks.", + }), + ); } else { panel.innerHTML = `

Select a category on the left to run audits.

`; } @@ -439,8 +553,7 @@ export const uiMethods = { // ──────────────────────────────────────────────────────────────────────── /** - * Footer row: Run All Tests + Reset button + credit line. - * Sets this.runAllButton and this.resetButton as side effects. + * Footer row: credit line only. * * @returns {HTMLDivElement} */ @@ -448,75 +561,6 @@ export const uiMethods = { const footer = document.createElement("div"); footer.className = "mageforge-toolbar-menu-footer"; - // ── Button row ────────────────────────────────────────────────────── - const btnRow = document.createElement("div"); - btnRow.className = "mageforge-toolbar-menu-button-row"; - - 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 = ` - - RUN ALL TESTS - `; - this.runAllButton.onclick = (e) => { - e.stopPropagation(); - this.runAllAuditsForScore(); - }; - this.runAllButton.onkeydown = (e) => { - 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(); - } - }; - btnRow.appendChild(this.runAllButton); - - 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", - ); - this.resetButton.innerHTML = - ''; - this.resetButton.onclick = (e) => { - e.stopPropagation(); - this.resetScore(); - }; - this.resetButton.onkeydown = (e) => { - 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(); - } - }; - btnRow.appendChild(this.resetButton); - - footer.appendChild(btnRow); - // ── Credit line ───────────────────────────────────────────────────── const credit = document.createElement("div"); credit.className = "mageforge-toolbar-menu-credit"; @@ -815,6 +859,32 @@ export const uiMethods = { }); }, + /** + * Update the score ring in a specific group panel header. + * + * @param {string} groupKey + * @param {number} score + */ + updateGroupScore(groupKey, score) { + if (!this.menu) return; + const CIRCUMFERENCE = 113.1; + + const panel = this.menu.querySelector(`[data-panel="${groupKey}"]`); + if (!panel) return; + + const ring = panel.querySelector(".mageforge-score-ring"); + if (ring) { + ring.setAttribute( + "stroke-dasharray", + `${((score / 100) * CIRCUMFERENCE).toFixed(2)} ${CIRCUMFERENCE}`, + ); + } + const number = panel.querySelector(".mageforge-score-number"); + if (number) { + number.textContent = score; + } + }, + /** * Reset all score displays and deactivate all audits. */ From bd215d46a92a70bbaf959d181b1a1f3c090d34ee Mon Sep 17 00:00:00 2001 From: Mathias Elle Date: Wed, 17 Jun 2026 01:38:14 +0200 Subject: [PATCH 06/48] feat: Add error/warning summary badges and update toolbar styles --- .../frontend/web/css/toolbar/_buttons.css | 51 +++++++++++- src/view/frontend/web/css/toolbar/_credit.css | 2 +- src/view/frontend/web/css/toolbar/_footer.css | 2 +- src/view/frontend/web/css/toolbar/_groups.css | 39 ++++++++- src/view/frontend/web/css/toolbar/_menu.css | 4 +- src/view/frontend/web/js/toolbar/audits.js | 2 + src/view/frontend/web/js/toolbar/ui.js | 82 ++++++++++++++++++- 7 files changed, 174 insertions(+), 8 deletions(-) diff --git a/src/view/frontend/web/css/toolbar/_buttons.css b/src/view/frontend/web/css/toolbar/_buttons.css index ecb6706..bc2aadb 100644 --- a/src/view/frontend/web/css/toolbar/_buttons.css +++ b/src/view/frontend/web/css/toolbar/_buttons.css @@ -164,6 +164,56 @@ border-color: var(--mageforge-border-glass); } +/* ── Home panel: Error / Warning summary badges ─────────────────────── */ + +.mageforge-home-summary { + display: flex; + align-items: center; + justify-content: center; + gap: 16px; + padding: 8px 16px 4px; +} + +.mageforge-home-summary-item { + display: flex; + align-items: baseline; + gap: 4px; + padding: 6px 12px; + border-radius: 8px; + background: var(--mageforge-surface-glass); + border: 1px solid var(--mageforge-border-color); +} + +.mageforge-home-summary-errors { + border-color: rgba(239, 68, 68, 0.3); +} + +.mageforge-home-summary-warnings { + border-color: rgba(245, 158, 11, 0.3); +} + +.mageforge-home-summary-count { + font-size: 18px; + font-weight: 700; + line-height: 1; +} + +.mageforge-home-summary-errors .mageforge-home-summary-count { + color: var(--mageforge-color-red); +} + +.mageforge-home-summary-warnings .mageforge-home-summary-count { + color: var(--mageforge-color-amber); +} + +.mageforge-home-summary-label { + font-size: 10px; + font-weight: 600; + color: var(--mageforge-color-slate-400); + text-transform: uppercase; + letter-spacing: 0.05em; +} + /* ── Group panels: Run [Group] Checks button ───────────────────────── */ .mageforge-group-run-btn { @@ -172,7 +222,6 @@ align-items: center; justify-content: center; gap: 8px; - width: 100%; padding: 10px 16px; background: linear-gradient( 90deg, diff --git a/src/view/frontend/web/css/toolbar/_credit.css b/src/view/frontend/web/css/toolbar/_credit.css index fad64dc..84352eb 100644 --- a/src/view/frontend/web/css/toolbar/_credit.css +++ b/src/view/frontend/web/css/toolbar/_credit.css @@ -8,7 +8,7 @@ */ .mageforge-toolbar-menu-credit { - margin-top: 8px; + padding-bottom: 4px; text-align: center; font-family: var(--mageforge-font-family); font-size: 10px; diff --git a/src/view/frontend/web/css/toolbar/_footer.css b/src/view/frontend/web/css/toolbar/_footer.css index 10d97da..327e438 100644 --- a/src/view/frontend/web/css/toolbar/_footer.css +++ b/src/view/frontend/web/css/toolbar/_footer.css @@ -9,6 +9,6 @@ .mageforge-toolbar-menu-footer { border-top: 1px solid var(--mageforge-border-color); - margin-top: 4px; + margin-top: auto; /* pin to bottom of flex container */; /* pin to bottom of flex container */ padding-top: 6px; } diff --git a/src/view/frontend/web/css/toolbar/_groups.css b/src/view/frontend/web/css/toolbar/_groups.css index 4ed9a69..1f550f3 100644 --- a/src/view/frontend/web/css/toolbar/_groups.css +++ b/src/view/frontend/web/css/toolbar/_groups.css @@ -16,6 +16,7 @@ .mageforge-toolbar-tabs { display: flex; flex-direction: row; + flex: 1; } /* ============================================================================ @@ -25,7 +26,7 @@ .mageforge-toolbar-tab-nav { display: flex; flex-direction: column; - width: auto; + width: 200px; flex-shrink: 0; border-right: 1px solid var(--mageforge-border-color); padding: 6px 4px; @@ -112,6 +113,40 @@ white-space: normal; } +/* ── Tab badges (error/warning counts) ─────────────────────────────── */ + +.mageforge-tab-badges { + display: flex; + align-items: center; + gap: 4px; + margin-top: 4px; + justify-self: center; +} + +.mageforge-tab-badge { + display: none; + align-items: center; + gap: 2px; + font-size: 10px; + font-weight: 700; + line-height: 1; + padding: 2px 6px; + border-radius: 10px; + white-space: nowrap; +} + +.mageforge-tab-badge--errors { + background: rgba(239, 68, 68, 0.15); + color: var(--mageforge-color-red); + border: 1px solid rgba(239, 68, 68, 0.3); +} + +.mageforge-tab-badge--warnings { + background: rgba(245, 158, 11, 0.15); + color: var(--mageforge-color-amber); + border: 1px solid rgba(245, 158, 11, 0.3); +} + /* ============================================================================ Tab Content (right column) ========================================================================== */ @@ -119,6 +154,7 @@ .mageforge-toolbar-tab-content { flex: 1; min-width: 0; + min-height: 0; /* allow flex-shrink below content size */ padding: 4px 0; } @@ -149,6 +185,7 @@ overflow-x: hidden; padding: 8px; gap: 8px; + height:50%; } /* ── Home panel ── */ diff --git a/src/view/frontend/web/css/toolbar/_menu.css b/src/view/frontend/web/css/toolbar/_menu.css index 92b6de8..89096a0 100644 --- a/src/view/frontend/web/css/toolbar/_menu.css +++ b/src/view/frontend/web/css/toolbar/_menu.css @@ -32,9 +32,9 @@ 0 -8px 24px var(--mageforge-shadow-lg), 0 6px 10px var(--mageforge-shadow-sm); padding: 0; - min-width: 550px; + min-width: 600px; max-height: min(90vh, 1000px); - min-height: 60vh; + min-height: 400px; overflow: hidden; font-family: var(--mageforge-font-family); /* flex-column keeps the footer pinned; the tab section fills remaining height */ diff --git a/src/view/frontend/web/js/toolbar/audits.js b/src/view/frontend/web/js/toolbar/audits.js index 6d881a1..33c743c 100644 --- a/src/view/frontend/web/js/toolbar/audits.js +++ b/src/view/frontend/web/js/toolbar/audits.js @@ -91,6 +91,7 @@ export const auditMethods = { const score = maxPoints > 0 ? Math.round((totalPoints / maxPoints) * 100) : 100; this.updateHealthScore(score); + this.updateHomeSummary(); } finally { this._batchRunning = false; btn.disabled = false; @@ -155,6 +156,7 @@ export const auditMethods = { const score = maxPoints > 0 ? Math.round((totalPoints / maxPoints) * 100) : 100; this.updateGroupScore(groupKey, score); + this.updateHomeSummary(); } finally { this._batchRunning = false; btn.disabled = false; diff --git a/src/view/frontend/web/js/toolbar/ui.js b/src/view/frontend/web/js/toolbar/ui.js index ee27f5c..eb7623e 100644 --- a/src/view/frontend/web/js/toolbar/ui.js +++ b/src/view/frontend/web/js/toolbar/ui.js @@ -220,6 +220,10 @@ export const uiMethods = { btn.innerHTML = ` ${label.split(" ")[0]} + + + + `; btn.onclick = (e) => { e.stopPropagation(); @@ -491,7 +495,7 @@ export const uiMethods = { this.runAllButton.className = "mageforge-home-check-btn"; this.runAllButton.innerHTML = ` - Check Health Score + Perform Full Check `; this.runAllButton.onclick = (e) => { e.stopPropagation(); @@ -499,7 +503,7 @@ export const uiMethods = { }; btnRow.appendChild(this.runAllButton); - // Reset button next to Check Health Score + // Reset button next to Perform Full Check this.resetButton = document.createElement("div"); this.resetButton.setAttribute("role", "button"); this.resetButton.setAttribute("tabindex", "0"); @@ -728,6 +732,16 @@ export const uiMethods = { const container = item.querySelector(".mageforge-audit-findings"); if (!container) return; + // Store finding counts on the item for badge aggregation + let errorCount = 0; + let warningCount = 0; + findings?.forEach((f) => { + if (f.severity === 'warning') warningCount++; + else errorCount++; + }); + item.dataset.findingErrors = String(errorCount); + item.dataset.findingWarnings = String(warningCount); + container.innerHTML = ""; container.classList.remove("mageforge-findings-open"); @@ -806,11 +820,72 @@ export const uiMethods = { } this.updateToggleAllButton(); + this.updateHomeSummary(); }, /** No-op – retained for compatibility. */ updateToggleAllButton() {}, + /** + * Update error/warning badges on the left navigation tabs. + * Counts actual findings (affected elements), not just audits. + */ + updateHomeSummary() { + if (!this.menu) return; + + // Count actual findings (elements) per group + const groupCounts = {}; + this.menu.querySelectorAll('[data-audit-key]').forEach((item) => { + const errors = parseInt(item.dataset.findingErrors || '0', 10); + const warnings = parseInt(item.dataset.findingWarnings || '0', 10); + + if (!errors && !warnings) return; + + const groupKey = item.dataset.groupKey; + if (!groupKey) return; + + if (!groupCounts[groupKey]) { + groupCounts[groupKey] = { errors: 0, warnings: 0 }; + } + + groupCounts[groupKey].errors += errors; + groupCounts[groupKey].warnings += warnings; + }); + + // Reset ALL badges first, then populate only those with findings + this.menu.querySelectorAll('[data-tab-badges-for]').forEach((container) => { + const errorBadge = container.querySelector('[data-type="errors"]'); + const warningBadge = container.querySelector('[data-type="warnings"]'); + if (errorBadge) { + errorBadge.textContent = ''; + errorBadge.style.display = 'none'; + } + if (warningBadge) { + warningBadge.textContent = ''; + warningBadge.style.display = 'none'; + } + }); + + // Update badges for each group tab with findings + Object.entries(groupCounts).forEach(([groupKey, counts]) => { + const container = this.menu.querySelector(`[data-tab-badges-for="${groupKey}"]`); + if (!container) return; + + const errorBadge = container.querySelector('[data-type="errors"]'); + const warningBadge = container.querySelector('[data-type="warnings"]'); + + if (counts.errors > 0 && errorBadge) { + errorBadge.textContent = counts.errors; + errorBadge.style.display = 'inline-flex'; + } + + if (counts.warnings > 0 && warningBadge) { + warningBadge.textContent = counts.warnings; + warningBadge.style.display = 'inline-flex'; + } + }); + }, + // ──────────────────────────────────────────────────────────────────────── // Health score // ──────────────────────────────────────────────────────────────────────── @@ -911,6 +986,9 @@ export const uiMethods = { this.menu.querySelectorAll(".mageforge-score-number").forEach((el) => { el.textContent = "--"; }); + + // Reset all navigation badges + this.updateHomeSummary(); }, // ──────────────────────────────────────────────────────────────────────── From c0b94000f0c25cd4515987a45674e975a651e18c Mon Sep 17 00:00:00 2001 From: Mathias Elle Date: Wed, 17 Jun 2026 01:39:27 +0200 Subject: [PATCH 07/48] chore: Remove hiding of group reset buttons in audit methods --- src/view/frontend/web/js/toolbar/audits.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/view/frontend/web/js/toolbar/audits.js b/src/view/frontend/web/js/toolbar/audits.js index 33c743c..d95b497 100644 --- a/src/view/frontend/web/js/toolbar/audits.js +++ b/src/view/frontend/web/js/toolbar/audits.js @@ -195,12 +195,6 @@ export const auditMethods = { this.setAuditActive(key, false); }); - // Hide all group reset buttons - this.getAuditGroups().forEach((group) => { - const resetBtn = this[`groupResetButton-${group.key}`]; - if (resetBtn) resetBtn.style.display = "none"; - }); - this.updateToggleAllButton(); }, From fe7bdcb373c46181999f5d788e3cc9ace5666d2b Mon Sep 17 00:00:00 2001 From: Mathias Elle Date: Wed, 17 Jun 2026 17:01:27 +0200 Subject: [PATCH 08/48] feat: Update toolbar styles and add custom scrollbar design --- src/view/frontend/web/css/toolbar/_footer.css | 2 +- src/view/frontend/web/css/toolbar/_groups.css | 30 +++++++++++++++++-- src/view/frontend/web/css/toolbar/_menu.css | 5 ++-- .../frontend/web/css/toolbar/_variables.css | 6 ++++ src/view/frontend/web/js/toolbar/ui.js | 30 +++++++++++-------- 5 files changed, 54 insertions(+), 19 deletions(-) diff --git a/src/view/frontend/web/css/toolbar/_footer.css b/src/view/frontend/web/css/toolbar/_footer.css index 327e438..f02dfbe 100644 --- a/src/view/frontend/web/css/toolbar/_footer.css +++ b/src/view/frontend/web/css/toolbar/_footer.css @@ -9,6 +9,6 @@ .mageforge-toolbar-menu-footer { border-top: 1px solid var(--mageforge-border-color); - margin-top: auto; /* pin to bottom of flex container */; /* pin to bottom of flex container */ + margin-top: auto; /* pin to bottom of flex container */ /* pin to bottom of flex container */ padding-top: 6px; } diff --git a/src/view/frontend/web/css/toolbar/_groups.css b/src/view/frontend/web/css/toolbar/_groups.css index 1f550f3..09e3a87 100644 --- a/src/view/frontend/web/css/toolbar/_groups.css +++ b/src/view/frontend/web/css/toolbar/_groups.css @@ -25,7 +25,7 @@ .mageforge-toolbar-tab-nav { display: flex; - flex-direction: column; + flex-direction: column-reverse; width: 200px; flex-shrink: 0; border-right: 1px solid var(--mageforge-border-color); @@ -183,9 +183,35 @@ flex: 1; overflow-y: auto; overflow-x: hidden; + max-height: 600px; padding: 8px; gap: 8px; - height:50%; + height: 50%; +} + +.mageforge-tab-panel-body::-webkit-scrollbar { + width: var(--mageforge-scrollbar-width); +} + +.mageforge-tab-panel-body::-webkit-scrollbar-track { + background: var(--mageforge-scrollbar-bg); +} + +.mageforge-tab-panel-body::-webkit-scrollbar-thumb { + background: var(--mageforge-scrollbar-thumb-bg); + border-radius: 100vw; + transition: background 0.2s ease; +} + +.mageforge-tab-panel-body::-webkit-scrollbar-thumb:hover { + background: var(--mageforge-scrollbar-thumb-hover-bg); +} + +/* Firefox */ +.mageforge-tab-panel-body { + scrollbar-width: thin; + scrollbar-color: var(--mageforge-scrollbar-thumb-bg) + var(--mageforge-scrollbar-bg); } /* ── Home panel ── */ diff --git a/src/view/frontend/web/css/toolbar/_menu.css b/src/view/frontend/web/css/toolbar/_menu.css index 89096a0..0b41260 100644 --- a/src/view/frontend/web/css/toolbar/_menu.css +++ b/src/view/frontend/web/css/toolbar/_menu.css @@ -33,8 +33,7 @@ 0 6px 10px var(--mageforge-shadow-sm); padding: 0; min-width: 600px; - max-height: min(90vh, 1000px); - min-height: 400px; + max-height: min(75vh, 1000px); overflow: hidden; font-family: var(--mageforge-font-family); /* flex-column keeps the footer pinned; the tab section fills remaining height */ @@ -245,7 +244,7 @@ ========================================================================== */ .mageforge-toolbar-menu-label { - font-size: 12px; + font-size: 13px; font-weight: 600; color: var(--mageforge-color-white); } diff --git a/src/view/frontend/web/css/toolbar/_variables.css b/src/view/frontend/web/css/toolbar/_variables.css index df51203..4907c48 100644 --- a/src/view/frontend/web/css/toolbar/_variables.css +++ b/src/view/frontend/web/css/toolbar/_variables.css @@ -70,4 +70,10 @@ var(--mageforge-color-pink) 70%, var(--mageforge-color-amber) 100% ); + + /* Scrollbar */ + --mageforge-scrollbar-width: 4px; + --mageforge-scrollbar-bg: transparent; + --mageforge-scrollbar-thumb-bg: var(--mageforge-color-blue); + --mageforge-scrollbar-thumb-hover-bg: var(--mageforge-color-blue-alpha-35); } diff --git a/src/view/frontend/web/js/toolbar/ui.js b/src/view/frontend/web/js/toolbar/ui.js index eb7623e..a519349 100644 --- a/src/view/frontend/web/js/toolbar/ui.js +++ b/src/view/frontend/web/js/toolbar/ui.js @@ -290,7 +290,9 @@ export const uiMethods = { groupBtn.className = "mageforge-group-run-btn"; groupBtn.dataset.group = group.key; this[`runGroupButton-${group.key}`] = groupBtn; - const groupLabel = this.getAuditGroups().find((g) => g.key === group.key)?.label ?? group.key; + const groupLabel = + this.getAuditGroups().find((g) => g.key === group.key)?.label ?? + group.key; groupBtn.innerHTML = ` Run ${groupLabel} Checks @@ -736,7 +738,7 @@ export const uiMethods = { let errorCount = 0; let warningCount = 0; findings?.forEach((f) => { - if (f.severity === 'warning') warningCount++; + if (f.severity === "warning") warningCount++; else errorCount++; }); item.dataset.findingErrors = String(errorCount); @@ -835,9 +837,9 @@ export const uiMethods = { // Count actual findings (elements) per group const groupCounts = {}; - this.menu.querySelectorAll('[data-audit-key]').forEach((item) => { - const errors = parseInt(item.dataset.findingErrors || '0', 10); - const warnings = parseInt(item.dataset.findingWarnings || '0', 10); + this.menu.querySelectorAll("[data-audit-key]").forEach((item) => { + const errors = parseInt(item.dataset.findingErrors || "0", 10); + const warnings = parseInt(item.dataset.findingWarnings || "0", 10); if (!errors && !warnings) return; @@ -853,22 +855,24 @@ export const uiMethods = { }); // Reset ALL badges first, then populate only those with findings - this.menu.querySelectorAll('[data-tab-badges-for]').forEach((container) => { + this.menu.querySelectorAll("[data-tab-badges-for]").forEach((container) => { const errorBadge = container.querySelector('[data-type="errors"]'); const warningBadge = container.querySelector('[data-type="warnings"]'); if (errorBadge) { - errorBadge.textContent = ''; - errorBadge.style.display = 'none'; + errorBadge.textContent = ""; + errorBadge.style.display = "none"; } if (warningBadge) { - warningBadge.textContent = ''; - warningBadge.style.display = 'none'; + warningBadge.textContent = ""; + warningBadge.style.display = "none"; } }); // Update badges for each group tab with findings Object.entries(groupCounts).forEach(([groupKey, counts]) => { - const container = this.menu.querySelector(`[data-tab-badges-for="${groupKey}"]`); + const container = this.menu.querySelector( + `[data-tab-badges-for="${groupKey}"]`, + ); if (!container) return; const errorBadge = container.querySelector('[data-type="errors"]'); @@ -876,12 +880,12 @@ export const uiMethods = { if (counts.errors > 0 && errorBadge) { errorBadge.textContent = counts.errors; - errorBadge.style.display = 'inline-flex'; + errorBadge.style.display = "inline-flex"; } if (counts.warnings > 0 && warningBadge) { warningBadge.textContent = counts.warnings; - warningBadge.style.display = 'inline-flex'; + warningBadge.style.display = "inline-flex"; } }); }, From be65dea4ead549ef2eca1c41899a6a9651cb6844 Mon Sep 17 00:00:00 2001 From: Mathias Elle Date: Mon, 22 Jun 2026 10:33:49 +0200 Subject: [PATCH 09/48] feat: Redesign footer action bar and update button functionality in toolbar --- src/view/frontend/web/css/toolbar/_footer.css | 15 ++++- src/view/frontend/web/css/toolbar/_menu.css | 1 + src/view/frontend/web/js/toolbar/ui.js | 62 ++++++++++++++----- 3 files changed, 60 insertions(+), 18 deletions(-) diff --git a/src/view/frontend/web/css/toolbar/_footer.css b/src/view/frontend/web/css/toolbar/_footer.css index f02dfbe..a4e7614 100644 --- a/src/view/frontend/web/css/toolbar/_footer.css +++ b/src/view/frontend/web/css/toolbar/_footer.css @@ -9,6 +9,19 @@ .mageforge-toolbar-menu-footer { border-top: 1px solid var(--mageforge-border-color); - margin-top: auto; /* pin to bottom of flex container */ /* pin to bottom of flex container */ + margin-top: auto; /* pin to bottom of flex container */ padding-top: 6px; } + +/* ── Footer action bar (dynamic run/reset buttons for current tab) ─── */ + +.mageforge-footer-action-bar { + padding: 6px 12px 2px; +} + +.mageforge-footer-btn-row { + display: flex; + align-items: center; + gap: 8px; + width: 100%; +} diff --git a/src/view/frontend/web/css/toolbar/_menu.css b/src/view/frontend/web/css/toolbar/_menu.css index 0b41260..bd9e08f 100644 --- a/src/view/frontend/web/css/toolbar/_menu.css +++ b/src/view/frontend/web/css/toolbar/_menu.css @@ -33,6 +33,7 @@ 0 6px 10px var(--mageforge-shadow-sm); padding: 0; min-width: 600px; + min-height: 700px; max-height: min(75vh, 1000px); overflow: hidden; font-family: var(--mageforge-font-family); diff --git a/src/view/frontend/web/js/toolbar/ui.js b/src/view/frontend/web/js/toolbar/ui.js index a519349..955a7e6 100644 --- a/src/view/frontend/web/js/toolbar/ui.js +++ b/src/view/frontend/web/js/toolbar/ui.js @@ -281,18 +281,16 @@ export const uiMethods = { const body = document.createElement("div"); body.className = "mageforge-tab-panel-body"; - // Group-specific run button row at the top of the panel body - const groupBtnRow = document.createElement("div"); - groupBtnRow.className = "mageforge-group-btn-row"; + const groupLabel = + this.getAuditGroups().find((g) => g.key === group.key)?.label ?? + group.key; + // Build run button – stored as ref, rendered in footer action bar const groupBtn = document.createElement("button"); groupBtn.type = "button"; groupBtn.className = "mageforge-group-run-btn"; groupBtn.dataset.group = group.key; this[`runGroupButton-${group.key}`] = groupBtn; - const groupLabel = - this.getAuditGroups().find((g) => g.key === group.key)?.label ?? - group.key; groupBtn.innerHTML = ` Run ${groupLabel} Checks @@ -301,9 +299,8 @@ export const uiMethods = { e.stopPropagation(); this.runGroupAuditsForScore(group.key); }; - groupBtnRow.appendChild(groupBtn); - // Reset button (hidden by default, shown after run) + // Build reset button – stored as ref, rendered in footer action bar const groupResetBtn = document.createElement("div"); groupResetBtn.setAttribute("role", "button"); groupResetBtn.setAttribute("tabindex", "0"); @@ -330,13 +327,8 @@ export const uiMethods = { this.resetGroupAudits(group.key); } }; - groupBtnRow.appendChild(groupResetBtn); - - // Store reference for showing/hiding this[`groupResetButton-${group.key}`] = groupResetBtn; - body.appendChild(groupBtnRow); - items.forEach((audit) => { body.appendChild( this.createMenuItem( @@ -494,7 +486,7 @@ export const uiMethods = { this.runAllButton = document.createElement("button"); this.runAllButton.type = "button"; - this.runAllButton.className = "mageforge-home-check-btn"; + this.runAllButton.className = "mageforge-group-run-btn"; this.runAllButton.innerHTML = ` Perform Full Check @@ -509,7 +501,7 @@ export const uiMethods = { this.resetButton = document.createElement("div"); this.resetButton.setAttribute("role", "button"); this.resetButton.setAttribute("tabindex", "0"); - this.resetButton.className = "mageforge-home-reset-btn"; + this.resetButton.className = "mageforge-group-reset-btn"; this.resetButton.title = "Reset score and deactivate all audits"; this.resetButton.setAttribute( "aria-label", @@ -538,8 +530,7 @@ export const uiMethods = { } }; btnRow.appendChild(this.resetButton); - - panel.appendChild(btnRow); + // btnRow held as ref; rendered in footer action bar via _updateFooterActions panel.appendChild( Object.assign(document.createElement("p"), { @@ -567,6 +558,11 @@ export const uiMethods = { const footer = document.createElement("div"); footer.className = "mageforge-toolbar-menu-footer"; + // ── Dynamic action bar (buttons for current tab) ───────────────────── + this.footerActionBar = document.createElement("div"); + this.footerActionBar.className = "mageforge-footer-action-bar"; + footer.appendChild(this.footerActionBar); + // ── Credit line ───────────────────────────────────────────────────── const credit = document.createElement("div"); credit.className = "mageforge-toolbar-menu-credit"; @@ -574,9 +570,39 @@ export const uiMethods = { 'Built with \u2764 by MageForge'; footer.appendChild(credit); + // Populate for the initially active tab (home) + this._updateFooterActions("home"); + return footer; }, + /** + * Populate the footer action bar with the run/reset buttons for the given tab. + * + * @param {string} key – Tab key ("home" or a group key like "wcag") + */ + _updateFooterActions(key) { + if (!this.footerActionBar) return; + this.footerActionBar.innerHTML = ""; + + const row = document.createElement("div"); + row.className = "mageforge-footer-btn-row"; + + if (key === "home") { + if (!this.runAllButton) return; + row.appendChild(this.runAllButton); + row.appendChild(this.resetButton); + } else { + const runBtn = this[`runGroupButton-${key}`]; + const resetBtn = this[`groupResetButton-${key}`]; + if (!runBtn) return; + row.appendChild(runBtn); + if (resetBtn) row.appendChild(resetBtn); + } + + this.footerActionBar.appendChild(row); + }, + // ──────────────────────────────────────────────────────────────────────── // Burger / trigger button // ──────────────────────────────────────────────────────────────────────── @@ -651,6 +677,8 @@ export const uiMethods = { ? panel.removeAttribute("hidden") : panel.setAttribute("hidden", ""); }); + + this._updateFooterActions(key); }, // ──────────────────────────────────────────────────────────────────────── From 6202dcb6619186e2840cc1727449c97897817706 Mon Sep 17 00:00:00 2001 From: Mathias Elle Date: Mon, 22 Jun 2026 10:43:17 +0200 Subject: [PATCH 10/48] feat: Add feature request button to toolbar for audit requests --- .../frontend/web/css/toolbar/_buttons.css | 29 +++++++++++++++++++ src/view/frontend/web/js/toolbar/ui.js | 15 ++++++++++ 2 files changed, 44 insertions(+) diff --git a/src/view/frontend/web/css/toolbar/_buttons.css b/src/view/frontend/web/css/toolbar/_buttons.css index bc2aadb..89f0ff7 100644 --- a/src/view/frontend/web/css/toolbar/_buttons.css +++ b/src/view/frontend/web/css/toolbar/_buttons.css @@ -164,6 +164,35 @@ border-color: var(--mageforge-border-glass); } +/* ── Feature Request link button ───────────────────────────────────── */ + +.mageforge-feature-request-btn { + display: inline-flex; + align-items: center; + gap: 6px; + margin: 8px 12px 4px; + padding: 6px 10px; + background: none; + border: 1px dashed var(--mageforge-border-color); + border-radius: 6px; + color: var(--mageforge-color-slate-400); + font-family: var(--mageforge-font-family); + font-size: 11px; + font-weight: 500; + text-decoration: none; + cursor: pointer; + transition: + color 0.15s ease, + border-color 0.15s ease, + background 0.15s ease; +} + +.mageforge-feature-request-btn:hover { + color: var(--mageforge-color-white); + border-color: var(--mageforge-border-glass); + background: var(--mageforge-surface-glass-hover); +} + /* ── Home panel: Error / Warning summary badges ─────────────────────── */ .mageforge-home-summary { diff --git a/src/view/frontend/web/js/toolbar/ui.js b/src/view/frontend/web/js/toolbar/ui.js index 955a7e6..119c2c0 100644 --- a/src/view/frontend/web/js/toolbar/ui.js +++ b/src/view/frontend/web/js/toolbar/ui.js @@ -341,6 +341,21 @@ export const uiMethods = { ), ); }); + + if (items.length < 6) { + const featureBtn = document.createElement("a"); + featureBtn.href = + "https://github.com/OpenForgeProject/mageforge/issues/new?labels=enhancement&template=feature_request.yml&title=%5BAudit+Request%5D+"; + featureBtn.target = "_blank"; + featureBtn.rel = "noopener noreferrer"; + featureBtn.className = "mageforge-feature-request-btn"; + featureBtn.innerHTML = ` + + Request an Audit + `; + body.appendChild(featureBtn); + } + panel.appendChild(body); wrapper.appendChild(panel); }); From db08598eadced72ae7ae38c98acff450f47eb02d Mon Sep 17 00:00:00 2001 From: Mathias Elle Date: Mon, 22 Jun 2026 11:00:30 +0200 Subject: [PATCH 11/48] feat: Redesign nav action bar and update button styles in toolbar --- src/view/frontend/web/css/toolbar/_footer.css | 43 +++++++++++++++++-- src/view/frontend/web/js/toolbar/ui.js | 24 ++++++----- 2 files changed, 53 insertions(+), 14 deletions(-) diff --git a/src/view/frontend/web/css/toolbar/_footer.css b/src/view/frontend/web/css/toolbar/_footer.css index a4e7614..e6db43b 100644 --- a/src/view/frontend/web/css/toolbar/_footer.css +++ b/src/view/frontend/web/css/toolbar/_footer.css @@ -13,10 +13,11 @@ padding-top: 6px; } -/* ── Footer action bar (dynamic run/reset buttons for current tab) ─── */ +/* ── Nav action bar (run/reset buttons at the bottom of the tab nav) ── */ -.mageforge-footer-action-bar { - padding: 6px 12px 2px; +.mageforge-nav-action-bar { + border-top: 1px solid var(--mageforge-border-color); + padding: 8px 8px 4px; } .mageforge-footer-btn-row { @@ -25,3 +26,39 @@ gap: 8px; width: 100%; } + +.mageforge-nav-action-bar .mageforge-footer-btn-row { + flex-direction: column; + align-items: stretch; + gap: 8px; +} + +/* Compact button styles when rendered inside the narrow nav sidebar */ + +.mageforge-nav-action-bar .mageforge-group-run-btn { + font-size: 11px; + padding: 8px 8px; + letter-spacing: 0.02em; + white-space: normal; + line-height: 1.3; + text-align: left; + justify-content: flex-start; +} + +.mageforge-nav-action-bar .mageforge-group-reset-btn { + width: auto; + height: auto; + padding: 8px 10px; + gap: 6px; + font-family: var(--mageforge-font-family); + font-size: 11px; + font-weight: 600; + letter-spacing: 0.02em; + text-transform: uppercase; + line-height: 1; + white-space: nowrap; + justify-content: flex-start; + color: var(--mageforge-color-white); + background: var(--mageforge-surface-glass); + border-color: var(--mageforge-border-glass); +} diff --git a/src/view/frontend/web/js/toolbar/ui.js b/src/view/frontend/web/js/toolbar/ui.js index 119c2c0..9b50ca2 100644 --- a/src/view/frontend/web/js/toolbar/ui.js +++ b/src/view/frontend/web/js/toolbar/ui.js @@ -6,7 +6,7 @@ * _buildMenu() – Full menu popup container * _buildMenuHeader() – Sticky title bar (logo + name + close button) * _buildTabLayout() – Two-column tab container (nav | content) - * _buildTabNav() – Left-side navigation buttons + * _buildTabNav() – Left-side navigation buttons + action bar at bottom * _buildNavTab() – Single nav tab button * _buildTabPanels() – All content panels * _buildPanel() – Panel shell (role=tabpanel) @@ -14,7 +14,7 @@ * _buildScoreWidget() – Circular score ring (panel headers) * _buildHomePanel() – Overview panel with half-arc gauge * _buildSettingsPanel() – Settings placeholder - * _buildMenuFooter() – Run All Tests + Reset + credit + * _buildMenuFooter() – Credit line only (action bar is in nav) * _buildBurgerButton() – Persistent trigger button * * switchTab() – Activate a tab and show its panel @@ -187,6 +187,12 @@ export const uiMethods = { nav.setAttribute("role", "tablist"); nav.setAttribute("aria-label", "Audit categories"); + // Action bar pinned to the visual bottom of the nav. + // column-reverse means the first DOM child appears at the visual bottom. + this.footerActionBar = document.createElement("div"); + this.footerActionBar.className = "mageforge-nav-action-bar"; + nav.appendChild(this.footerActionBar); + nav.appendChild(this._buildNavTab("home", ICON_HOME, "Home", true)); this.getAuditGroups().forEach((group) => { @@ -293,7 +299,7 @@ export const uiMethods = { this[`runGroupButton-${group.key}`] = groupBtn; groupBtn.innerHTML = ` - Run ${groupLabel} Checks + Run Check `; groupBtn.onclick = (e) => { e.stopPropagation(); @@ -308,7 +314,7 @@ export const uiMethods = { groupResetBtn.setAttribute("aria-label", `Reset ${groupLabel} audits`); groupResetBtn.title = `Reset ${groupLabel} audits`; groupResetBtn.innerHTML = - ''; + ' Reset'; groupResetBtn.onclick = (e) => { e.stopPropagation(); this.resetGroupAudits(group.key); @@ -523,7 +529,7 @@ export const uiMethods = { "Reset score and deactivate all audits", ); this.resetButton.innerHTML = - ''; + ' Reset'; this.resetButton.onclick = (e) => { e.stopPropagation(); this.resetScore(); @@ -573,11 +579,6 @@ export const uiMethods = { const footer = document.createElement("div"); footer.className = "mageforge-toolbar-menu-footer"; - // ── Dynamic action bar (buttons for current tab) ───────────────────── - this.footerActionBar = document.createElement("div"); - this.footerActionBar.className = "mageforge-footer-action-bar"; - footer.appendChild(this.footerActionBar); - // ── Credit line ───────────────────────────────────────────────────── const credit = document.createElement("div"); credit.className = "mageforge-toolbar-menu-credit"; @@ -585,7 +586,8 @@ export const uiMethods = { 'Built with \u2764 by MageForge'; footer.appendChild(credit); - // Populate for the initially active tab (home) + // Populate nav action bar for the initially active tab (home). + // footerActionBar was already created in _buildTabNav(). this._updateFooterActions("home"); return footer; From d41762050367f960a2442125ffe7674a8e79c2cf Mon Sep 17 00:00:00 2001 From: Mathias Elle Date: Mon, 22 Jun 2026 11:32:33 +0200 Subject: [PATCH 12/48] feat: Update button styles and add reset all functionality in toolbar --- src/view/frontend/web/css/toolbar/_footer.css | 25 ++++++- src/view/frontend/web/js/toolbar/ui.js | 69 +++++++++++++++++-- 2 files changed, 88 insertions(+), 6 deletions(-) diff --git a/src/view/frontend/web/css/toolbar/_footer.css b/src/view/frontend/web/css/toolbar/_footer.css index e6db43b..75c63da 100644 --- a/src/view/frontend/web/css/toolbar/_footer.css +++ b/src/view/frontend/web/css/toolbar/_footer.css @@ -37,7 +37,7 @@ .mageforge-nav-action-bar .mageforge-group-run-btn { font-size: 11px; - padding: 8px 8px; + padding: 8px; letter-spacing: 0.02em; white-space: normal; line-height: 1.3; @@ -48,7 +48,7 @@ .mageforge-nav-action-bar .mageforge-group-reset-btn { width: auto; height: auto; - padding: 8px 10px; + padding: 8px 16px; gap: 6px; font-family: var(--mageforge-font-family); font-size: 11px; @@ -62,3 +62,24 @@ background: var(--mageforge-surface-glass); border-color: var(--mageforge-border-glass); } + +/* "Reset All" state: red tint to signal destructive scope */ + +.mageforge-nav-action-bar .mageforge-group-reset-btn.mageforge-group-reset-btn--all { + color: var(--mageforge-color-red, #ef4444); + background: rgba(239, 68, 68, 0.08); + border-color: rgba(239, 68, 68, 0.5); +} + +.mageforge-nav-action-bar .mageforge-group-reset-btn.mageforge-group-reset-btn--all:hover { + background: rgba(239, 68, 68, 0.15); + border-color: rgba(239, 68, 68, 0.75); +} + +/* Disabled state: nothing active in the current group */ + +.mageforge-nav-action-bar .mageforge-group-reset-btn.mageforge-group-reset-btn--disabled { + opacity: 0.35; + cursor: not-allowed; + pointer-events: none; +} diff --git a/src/view/frontend/web/js/toolbar/ui.js b/src/view/frontend/web/js/toolbar/ui.js index 9b50ca2..993d528 100644 --- a/src/view/frontend/web/js/toolbar/ui.js +++ b/src/view/frontend/web/js/toolbar/ui.js @@ -187,7 +187,7 @@ export const uiMethods = { nav.setAttribute("role", "tablist"); nav.setAttribute("aria-label", "Audit categories"); - // Action bar pinned to the visual bottom of the nav. + // Action bar (run/reset for the current tab). // column-reverse means the first DOM child appears at the visual bottom. this.footerActionBar = document.createElement("div"); this.footerActionBar.className = "mageforge-nav-action-bar"; @@ -204,6 +204,56 @@ export const uiMethods = { return nav; }, + /** + * When audits from 2+ groups are active, relabel every group reset button + * to "Reset All" and wire it to reset everything; otherwise restore the + * per-group label and behaviour. + */ + _updateResetAllButton() { + const isMulti = this._isMultiGroupActive(); + const RESET_SVG = + ''; + const label = isMulti ? "Reset All" : "Reset"; + this.getAuditGroups().forEach((group) => { + const btn = this[`groupResetButton-${group.key}`]; + if (!btn) return; + btn.innerHTML = `${RESET_SVG} ${label}`; + const ariaLabel = isMulti + ? "Reset all active audits" + : `Reset ${group.label} audits`; + btn.setAttribute("aria-label", ariaLabel); + btn.title = ariaLabel; + btn.classList.toggle("mageforge-group-reset-btn--all", isMulti); + + const groupHasActive = + isMulti || + this.getAudits().some( + (a) => a.group === group.key && this.activeAudits.has(a.key), + ); + btn.classList.toggle("mageforge-group-reset-btn--disabled", !groupHasActive); + btn.setAttribute("aria-disabled", String(!groupHasActive)); + btn.setAttribute("tabindex", groupHasActive ? "0" : "-1"); + }); + + // Home reset button: disabled when nothing is active at all + if (this.resetButton) { + const hasAny = this.activeAudits.size > 0; + this.resetButton.classList.toggle("mageforge-group-reset-btn--disabled", !hasAny); + this.resetButton.setAttribute("aria-disabled", String(!hasAny)); + this.resetButton.setAttribute("tabindex", hasAny ? "0" : "-1"); + } + }, + + /** Returns true when audits from at least 2 different groups are active. */ + _isMultiGroupActive() { + const activeGroups = new Set( + this.getAudits() + .filter((a) => a.group && this.activeAudits.has(a.key)) + .map((a) => a.group), + ); + return activeGroups.size >= 2; + }, + /** * Single tab button (icon stacked above abbreviated label). * @@ -315,22 +365,31 @@ export const uiMethods = { groupResetBtn.title = `Reset ${groupLabel} audits`; groupResetBtn.innerHTML = ' Reset'; + const handleGroupReset = () => { + const btn = this[`groupResetButton-${group.key}`]; + if (btn?.classList.contains("mageforge-group-reset-btn--disabled")) return; + if (this._isMultiGroupActive()) { + this.resetScore(); + } else { + this.resetGroupAudits(group.key); + } + }; groupResetBtn.onclick = (e) => { e.stopPropagation(); - this.resetGroupAudits(group.key); + handleGroupReset(); }; groupResetBtn.onkeydown = (e) => { if (e.key === "Enter") { e.preventDefault(); e.stopPropagation(); - this.resetGroupAudits(group.key); + handleGroupReset(); } if (e.key === " ") e.preventDefault(); }; groupResetBtn.onkeyup = (e) => { if (e.key === " ") { e.stopPropagation(); - this.resetGroupAudits(group.key); + handleGroupReset(); } }; this[`groupResetButton-${group.key}`] = groupResetBtn; @@ -589,6 +648,7 @@ export const uiMethods = { // Populate nav action bar for the initially active tab (home). // footerActionBar was already created in _buildTabNav(). this._updateFooterActions("home"); + this._updateResetAllButton(); return footer; }, @@ -868,6 +928,7 @@ export const uiMethods = { this.updateToggleAllButton(); this.updateHomeSummary(); + this._updateResetAllButton(); }, /** No-op – retained for compatibility. */ From 87c252a048c1ddb31e8fa31a2869cb413e830eed Mon Sep 17 00:00:00 2001 From: Mathias Elle Date: Mon, 22 Jun 2026 11:33:46 +0200 Subject: [PATCH 13/48] feat: Update padding in nav action bar for improved layout --- src/view/frontend/web/css/toolbar/_footer.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/view/frontend/web/css/toolbar/_footer.css b/src/view/frontend/web/css/toolbar/_footer.css index 75c63da..eb4855f 100644 --- a/src/view/frontend/web/css/toolbar/_footer.css +++ b/src/view/frontend/web/css/toolbar/_footer.css @@ -17,7 +17,7 @@ .mageforge-nav-action-bar { border-top: 1px solid var(--mageforge-border-color); - padding: 8px 8px 4px; + padding: 12px 8px 6px; } .mageforge-footer-btn-row { From 227e81e8fd0100d68fcce5e8e1153091f999be02 Mon Sep 17 00:00:00 2001 From: Mathias Elle Date: Mon, 22 Jun 2026 11:35:07 +0200 Subject: [PATCH 14/48] feat: Update nav tab label from "Home" to "Dashboard" --- 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 993d528..d86f7fc 100644 --- a/src/view/frontend/web/js/toolbar/ui.js +++ b/src/view/frontend/web/js/toolbar/ui.js @@ -193,7 +193,7 @@ export const uiMethods = { this.footerActionBar.className = "mageforge-nav-action-bar"; nav.appendChild(this.footerActionBar); - nav.appendChild(this._buildNavTab("home", ICON_HOME, "Home", true)); + nav.appendChild(this._buildNavTab("home", ICON_HOME, "Dashboard", true)); this.getAuditGroups().forEach((group) => { nav.appendChild( From 45fa1858d4033a9d04fdc319f73dd4e23321523b Mon Sep 17 00:00:00 2001 From: Mathias Elle Date: Mon, 22 Jun 2026 11:40:18 +0200 Subject: [PATCH 15/48] feat: Implement dashboard category score breakdown and update UI interactions --- src/view/frontend/web/css/toolbar/_groups.css | 39 +++++++++++++++++++ src/view/frontend/web/js/toolbar/audits.js | 33 ++++++++++++++++ src/view/frontend/web/js/toolbar/ui.js | 38 ++++++++++++++++++ 3 files changed, 110 insertions(+) diff --git a/src/view/frontend/web/css/toolbar/_groups.css b/src/view/frontend/web/css/toolbar/_groups.css index 09e3a87..cedc79f 100644 --- a/src/view/frontend/web/css/toolbar/_groups.css +++ b/src/view/frontend/web/css/toolbar/_groups.css @@ -280,6 +280,45 @@ color: var(--mageforge-color-white); } +/* ── Dashboard category score breakdown ── */ + +.mageforge-dashboard-categories { + display: flex; + flex-direction: column; + gap: 5px; + padding: 8px 12px 4px; +} + +.mageforge-dashboard-category { + display: flex; + align-items: center; + justify-content: space-between; + padding: 6px 10px; + border-radius: 6px; + background: rgba(255, 255, 255, 0.04); + border-left: 3px solid var(--category-color, var(--mageforge-color-slate-400)); +} + +.mageforge-dashboard-category-label { + font-family: var(--mageforge-font-family); + font-size: 11px; + font-weight: 500; + color: var(--mageforge-color-slate-400); + letter-spacing: 0.03em; +} + +.mageforge-dashboard-category-score { + font-family: var(--mageforge-font-family); + font-size: 12px; + font-weight: 700; + color: var(--mageforge-color-slate-400); + transition: color 0.3s ease; +} + +.mageforge-dashboard-category-score--active { + color: var(--category-color, var(--mageforge-color-white)); +} + /* ============================================================================ Tab Panel ========================================================================== */ diff --git a/src/view/frontend/web/js/toolbar/audits.js b/src/view/frontend/web/js/toolbar/audits.js index d95b497..1a889a6 100644 --- a/src/view/frontend/web/js/toolbar/audits.js +++ b/src/view/frontend/web/js/toolbar/audits.js @@ -91,6 +91,39 @@ export const auditMethods = { const score = maxPoints > 0 ? Math.round((totalPoints / maxPoints) * 100) : 100; this.updateHealthScore(score); + + // Update per-group scores on the dashboard + const groupScores = {}; + audits.forEach((audit) => { + if (!audit.group) return; + const item = this.menu?.querySelector( + `[data-audit-key="${audit.key}"]`, + ); + if (!item) return; + if (!groupScores[audit.group]) { + groupScores[audit.group] = { total: 0, max: 0 }; + } + groupScores[audit.group].max += 100; + const status = item.querySelector(".mageforge-toolbar-menu-status"); + if (!status || !status.textContent.trim()) { + groupScores[audit.group].total += 100; + } else if ( + status.classList.contains("mageforge-toolbar-menu-status--success") + ) { + groupScores[audit.group].total += 100; + } else if ( + status.classList.contains("mageforge-toolbar-menu-status--warning") + ) { + groupScores[audit.group].total += 50; + } + }); + Object.entries(groupScores).forEach(([groupKey, { total, max }]) => { + this.updateGroupScore( + groupKey, + max > 0 ? Math.round((total / max) * 100) : 100, + ); + }); + this.updateHomeSummary(); } finally { this._batchRunning = false; diff --git a/src/view/frontend/web/js/toolbar/ui.js b/src/view/frontend/web/js/toolbar/ui.js index d86f7fc..f7d6996 100644 --- a/src/view/frontend/web/js/toolbar/ui.js +++ b/src/view/frontend/web/js/toolbar/ui.js @@ -612,6 +612,24 @@ export const uiMethods = { btnRow.appendChild(this.resetButton); // btnRow held as ref; rendered in footer action bar via _updateFooterActions + // Category score breakdown + const categories = document.createElement("div"); + categories.className = "mageforge-dashboard-categories"; + this.getAuditGroups().forEach((group) => { + const card = document.createElement("div"); + card.className = "mageforge-dashboard-category"; + card.style.setProperty( + "--category-color", + `var(--mageforge-group-color-${group.key})`, + ); + card.innerHTML = ` + ${group.label} + -- + `; + categories.appendChild(card); + }); + panel.appendChild(categories); + panel.appendChild( Object.assign(document.createElement("p"), { className: "mageforge-home-hint", @@ -1068,6 +1086,18 @@ export const uiMethods = { if (number) { number.textContent = score; } + + // Also update the dashboard category badge + const dashboardScore = this.menu.querySelector( + `[data-dashboard-group-score="${groupKey}"]`, + ); + if (dashboardScore) { + dashboardScore.textContent = score; + dashboardScore.classList.toggle( + "mageforge-dashboard-category-score--active", + score > 0, + ); + } }, /** @@ -1097,6 +1127,14 @@ export const uiMethods = { el.textContent = "--"; }); + // Reset dashboard category badges + this.menu + .querySelectorAll("[data-dashboard-group-score]") + .forEach((el) => { + el.textContent = "--"; + el.classList.remove("mageforge-dashboard-category-score--active"); + }); + // Reset all navigation badges this.updateHomeSummary(); }, From 6e60478ae798440dde71db2aec82ea14e91cd31a Mon Sep 17 00:00:00 2001 From: Mathias Elle Date: Mon, 22 Jun 2026 11:44:59 +0200 Subject: [PATCH 16/48] feat: Add dashboard issues list and update functionality --- src/view/frontend/web/css/toolbar/_groups.css | 62 +++++++++++++++++++ src/view/frontend/web/js/toolbar/ui.js | 52 ++++++++++++++++ 2 files changed, 114 insertions(+) diff --git a/src/view/frontend/web/css/toolbar/_groups.css b/src/view/frontend/web/css/toolbar/_groups.css index cedc79f..45e5d7e 100644 --- a/src/view/frontend/web/css/toolbar/_groups.css +++ b/src/view/frontend/web/css/toolbar/_groups.css @@ -319,6 +319,68 @@ color: var(--category-color, var(--mageforge-color-white)); } +/* ── Dashboard issues list ── */ + +.mageforge-dashboard-issues { + display: flex; + flex-direction: column; + gap: 3px; + padding: 4px 12px 0; +} + +.mageforge-dashboard-issues-heading { + font-family: var(--mageforge-font-family); + font-size: 10px; + font-weight: 600; + letter-spacing: 0.06em; + text-transform: uppercase; + color: var(--mageforge-color-slate-400); + margin: 4px 0 4px; +} + +.mageforge-dashboard-issue { + display: flex; + align-items: center; + gap: 8px; + padding: 4px 8px; + border-radius: 5px; + background: rgba(255, 255, 255, 0.03); +} + +.mageforge-dashboard-issue--error { + border-left: 2px solid var(--mageforge-color-red); +} + +.mageforge-dashboard-issue--warning { + border-left: 2px solid var(--mageforge-color-amber); +} + +.mageforge-dashboard-issue-count { + font-family: var(--mageforge-font-family); + font-size: 11px; + font-weight: 700; + min-width: 20px; + text-align: right; + flex-shrink: 0; +} + +.mageforge-dashboard-issue--error .mageforge-dashboard-issue-count { + color: var(--mageforge-color-red); +} + +.mageforge-dashboard-issue--warning .mageforge-dashboard-issue-count { + color: var(--mageforge-color-amber); +} + +.mageforge-dashboard-issue-label { + font-family: var(--mageforge-font-family); + font-size: 11px; + color: var(--mageforge-color-slate-400); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + /* ============================================================================ Tab Panel ========================================================================== */ diff --git a/src/view/frontend/web/js/toolbar/ui.js b/src/view/frontend/web/js/toolbar/ui.js index f7d6996..17d7574 100644 --- a/src/view/frontend/web/js/toolbar/ui.js +++ b/src/view/frontend/web/js/toolbar/ui.js @@ -630,6 +630,11 @@ export const uiMethods = { }); panel.appendChild(categories); + // Issues list – populated by updateDashboardIssues() + this.dashboardIssuesEl = document.createElement("div"); + this.dashboardIssuesEl.className = "mageforge-dashboard-issues"; + panel.appendChild(this.dashboardIssuesEl); + panel.appendChild( Object.assign(document.createElement("p"), { className: "mageforge-home-hint", @@ -952,6 +957,48 @@ export const uiMethods = { /** No-op – retained for compatibility. */ updateToggleAllButton() {}, + /** + * Rebuild the compact issues list on the Dashboard panel. + */ + updateDashboardIssues() { + if (!this.dashboardIssuesEl) return; + + /** @type {Array<{label: string, count: number, severity: string}>} */ + const rows = []; + this.menu?.querySelectorAll("[data-audit-key]").forEach((item) => { + const errors = parseInt(item.dataset.findingErrors || "0", 10); + const warnings = parseInt(item.dataset.findingWarnings || "0", 10); + if (!errors && !warnings) return; + const label = + item.querySelector(".mageforge-toolbar-menu-label")?.textContent ?? ""; + if (errors) rows.push({ label, count: errors, severity: "error" }); + if (warnings) rows.push({ label, count: warnings, severity: "warning" }); + }); + + this.dashboardIssuesEl.innerHTML = ""; + + if (!rows.length) return; + + // Errors first, then warnings, each group sorted by count desc + rows.sort((a, b) => { + if (a.severity !== b.severity) + return a.severity === "error" ? -1 : 1; + return b.count - a.count; + }); + + const heading = document.createElement("p"); + heading.className = "mageforge-dashboard-issues-heading"; + heading.textContent = "Issues found"; + this.dashboardIssuesEl.appendChild(heading); + + rows.forEach(({ label, count, severity }) => { + const row = document.createElement("div"); + row.className = `mageforge-dashboard-issue mageforge-dashboard-issue--${severity}`; + row.innerHTML = `${count}${label}`; + this.dashboardIssuesEl.appendChild(row); + }); + }, + /** * Update error/warning badges on the left navigation tabs. * Counts actual findings (affected elements), not just audits. @@ -992,6 +1039,8 @@ export const uiMethods = { } }); + this.updateDashboardIssues(); + // Update badges for each group tab with findings Object.entries(groupCounts).forEach(([groupKey, counts]) => { const container = this.menu.querySelector( @@ -1135,6 +1184,9 @@ export const uiMethods = { el.classList.remove("mageforge-dashboard-category-score--active"); }); + // Clear dashboard issues list + if (this.dashboardIssuesEl) this.dashboardIssuesEl.innerHTML = ""; + // Reset all navigation badges this.updateHomeSummary(); }, From b54a14c26b668f543af45330ff4d46d1c0d44b7b Mon Sep 17 00:00:00 2001 From: Mathias Elle Date: Mon, 22 Jun 2026 11:47:50 +0200 Subject: [PATCH 17/48] feat: Update toolbar background color for improved visibility --- src/view/frontend/web/css/toolbar/_groups.css | 1 + 1 file changed, 1 insertion(+) diff --git a/src/view/frontend/web/css/toolbar/_groups.css b/src/view/frontend/web/css/toolbar/_groups.css index 45e5d7e..2469ec1 100644 --- a/src/view/frontend/web/css/toolbar/_groups.css +++ b/src/view/frontend/web/css/toolbar/_groups.css @@ -28,6 +28,7 @@ flex-direction: column-reverse; width: 200px; flex-shrink: 0; + background: var(--mageforge-bg-dark); border-right: 1px solid var(--mageforge-border-color); padding: 6px 4px; gap: 2px; From 2490ea044182aecadb753162da534568cc528dbf Mon Sep 17 00:00:00 2001 From: Mathias Elle Date: Mon, 22 Jun 2026 11:54:19 +0200 Subject: [PATCH 18/48] feat: Update active state styles for home tab in toolbar --- src/view/frontend/web/css/toolbar/_groups.css | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/view/frontend/web/css/toolbar/_groups.css b/src/view/frontend/web/css/toolbar/_groups.css index 2469ec1..cbbcee6 100644 --- a/src/view/frontend/web/css/toolbar/_groups.css +++ b/src/view/frontend/web/css/toolbar/_groups.css @@ -70,6 +70,11 @@ /* Active state – per-group colour accent */ +.mageforge-toolbar-tab-btn[data-tab="home"].mageforge-tab-active { + color: var(--mageforge-color-white); + background: rgba(255, 255, 255, 0.08); +} + .mageforge-toolbar-tab-btn[data-tab="wcag"].mageforge-tab-active { color: var(--mageforge-group-color-wcag); background: rgba(168, 85, 247, 0.1); From c759306437f7f214deba580c00849aabc5016aea Mon Sep 17 00:00:00 2001 From: Mathias Elle Date: Mon, 22 Jun 2026 12:00:43 +0200 Subject: [PATCH 19/48] feat: Add focus-visible styles for toolbar buttons and menu items --- .../frontend/web/css/toolbar/_buttons.css | 37 ++++++++++++++ src/view/frontend/web/css/toolbar/_groups.css | 50 ++----------------- src/view/frontend/web/css/toolbar/_menu.css | 10 ++++ 3 files changed, 52 insertions(+), 45 deletions(-) diff --git a/src/view/frontend/web/css/toolbar/_buttons.css b/src/view/frontend/web/css/toolbar/_buttons.css index 89f0ff7..d8d7d69 100644 --- a/src/view/frontend/web/css/toolbar/_buttons.css +++ b/src/view/frontend/web/css/toolbar/_buttons.css @@ -60,6 +60,11 @@ transform: none; } +.mageforge-toolbar-menu-run-all:focus-visible { + outline: 2px solid var(--mageforge-color-blue); + outline-offset: 2px; +} + .mageforge-toolbar-menu-reset { display: flex; align-items: center; @@ -83,6 +88,11 @@ border-color: var(--mageforge-border-glass); } +.mageforge-toolbar-menu-reset:focus-visible { + outline: 2px solid var(--mageforge-color-blue); + outline-offset: 2px; +} + /* ── Home panel: Check Health Score button ─────────────────────────── */ .mageforge-home-check-btn { @@ -131,6 +141,11 @@ transform: none; } +.mageforge-home-check-btn:focus-visible { + outline: 2px solid var(--mageforge-color-blue); + outline-offset: 2px; +} + /* ── Home panel: Check + Reset button row ──────────────────────────── */ .mageforge-home-btn-row { @@ -193,6 +208,11 @@ background: var(--mageforge-surface-glass-hover); } +.mageforge-feature-request-btn:focus-visible { + outline: 2px solid var(--mageforge-color-blue); + outline-offset: 2px; +} + /* ── Home panel: Error / Warning summary badges ─────────────────────── */ .mageforge-home-summary { @@ -290,6 +310,11 @@ transform: none; } +.mageforge-group-run-btn:focus-visible { + outline: 2px solid var(--mageforge-color-blue); + outline-offset: 2px; +} + /* ── Group panels: Run + Reset button row ──────────────────────────── */ .mageforge-group-btn-row { @@ -321,3 +346,15 @@ background: var(--mageforge-surface-glass-hover); border-color: var(--mageforge-border-glass); } + +.mageforge-group-reset-btn:disabled, +.mageforge-group-reset-btn[aria-disabled="true"] { + opacity: 0.35; + cursor: not-allowed; + pointer-events: none; +} + +.mageforge-group-reset-btn:focus-visible { + outline: 2px solid var(--mageforge-color-blue); + outline-offset: 2px; +} diff --git a/src/view/frontend/web/css/toolbar/_groups.css b/src/view/frontend/web/css/toolbar/_groups.css index cbbcee6..732ebce 100644 --- a/src/view/frontend/web/css/toolbar/_groups.css +++ b/src/view/frontend/web/css/toolbar/_groups.css @@ -68,6 +68,11 @@ background: var(--mageforge-surface-glass-hover); } +.mageforge-toolbar-tab-btn:focus-visible { + outline: 2px solid var(--mageforge-color-blue); + outline-offset: 2px; +} + /* Active state – per-group colour accent */ .mageforge-toolbar-tab-btn[data-tab="home"].mageforge-tab-active { @@ -192,7 +197,6 @@ max-height: 600px; padding: 8px; gap: 8px; - height: 50%; } .mageforge-tab-panel-body::-webkit-scrollbar { @@ -242,50 +246,6 @@ color: var(--mageforge-color-white); } -/* ── Home panel ── */ - -.mageforge-home-panel { - flex: 1; - overflow-y: auto; - padding: 8px 0 4px; -} - -.mageforge-home-hint { - font-family: var(--mageforge-font-family); - font-size: 11px; - color: var(--mageforge-color-slate-400); - text-align: center; - padding: 6px 16px 0; - margin: 0; - line-height: 1.5; -} - -.mageforge-home-hint strong { - color: var(--mageforge-color-white); -} - -/* ── Home panel ── */ - -.mageforge-home-panel { - flex: 1; - overflow-y: auto; - padding: 8px 0 4px; -} - -.mageforge-home-hint { - font-family: var(--mageforge-font-family); - font-size: 11px; - color: var(--mageforge-color-slate-400); - text-align: center; - padding: 6px 16px 0; - margin: 0; - line-height: 1.5; -} - -.mageforge-home-hint strong { - color: var(--mageforge-color-white); -} - /* ── Dashboard category score breakdown ── */ .mageforge-dashboard-categories { diff --git a/src/view/frontend/web/css/toolbar/_menu.css b/src/view/frontend/web/css/toolbar/_menu.css index bd9e08f..7e003b4 100644 --- a/src/view/frontend/web/css/toolbar/_menu.css +++ b/src/view/frontend/web/css/toolbar/_menu.css @@ -118,6 +118,11 @@ background: var(--mageforge-surface-glass-hover); } +.mageforge-toolbar-menu-close:focus-visible { + outline: 2px solid var(--mageforge-color-blue); + outline-offset: 2px; +} + /* ============================================================================ Menu Items ========================================================================== */ @@ -145,6 +150,11 @@ background: var(--mageforge-surface-glass-hover); } +.mageforge-toolbar-menu-item:focus-visible { + outline: 2px solid var(--mageforge-color-blue); + outline-offset: 2px; +} + .mageforge-toolbar-menu-item.mageforge-active { background: linear-gradient( 90deg, From fcb359eda2392da3270fdd5461714a93a1757188 Mon Sep 17 00:00:00 2001 From: Mathias Elle Date: Mon, 22 Jun 2026 12:14:22 +0200 Subject: [PATCH 20/48] feat: Enhance toolbar animations and button interactions with loading indicators --- .../frontend/web/css/toolbar/_animations.css | 6 ++++ .../frontend/web/css/toolbar/_buttons.css | 32 +++++++++++++++++++ .../frontend/web/css/toolbar/_findings.css | 14 ++++++++ src/view/frontend/web/js/toolbar/audits.js | 2 ++ src/view/frontend/web/js/toolbar/ui.js | 13 +++++--- 5 files changed, 63 insertions(+), 4 deletions(-) diff --git a/src/view/frontend/web/css/toolbar/_animations.css b/src/view/frontend/web/css/toolbar/_animations.css index 47a2be5..fd536de 100644 --- a/src/view/frontend/web/css/toolbar/_animations.css +++ b/src/view/frontend/web/css/toolbar/_animations.css @@ -26,3 +26,9 @@ opacity: 0; } } + +@keyframes mageforge-spin { + to { + transform: rotate(360deg); + } +} diff --git a/src/view/frontend/web/css/toolbar/_buttons.css b/src/view/frontend/web/css/toolbar/_buttons.css index d8d7d69..e78b501 100644 --- a/src/view/frontend/web/css/toolbar/_buttons.css +++ b/src/view/frontend/web/css/toolbar/_buttons.css @@ -60,6 +60,22 @@ transform: none; } +.mageforge-toolbar-menu-run-all.mageforge-running svg:first-child { + display: none; +} + +.mageforge-toolbar-menu-run-all.mageforge-running::before { + content: ""; + display: inline-block; + width: 14px; + height: 14px; + border: 2px solid rgba(255, 255, 255, 0.3); + border-top-color: var(--mageforge-color-white); + border-radius: 50%; + animation: mageforge-spin 0.7s linear infinite; + flex-shrink: 0; +} + .mageforge-toolbar-menu-run-all:focus-visible { outline: 2px solid var(--mageforge-color-blue); outline-offset: 2px; @@ -310,6 +326,22 @@ transform: none; } +.mageforge-group-run-btn.mageforge-running svg:first-child { + display: none; +} + +.mageforge-group-run-btn.mageforge-running::before { + content: ""; + display: inline-block; + width: 14px; + height: 14px; + border: 2px solid rgba(255, 255, 255, 0.3); + border-top-color: var(--mageforge-color-white); + border-radius: 50%; + animation: mageforge-spin 0.7s linear infinite; + flex-shrink: 0; +} + .mageforge-group-run-btn:focus-visible { outline: 2px solid var(--mageforge-color-blue); outline-offset: 2px; diff --git a/src/view/frontend/web/css/toolbar/_findings.css b/src/view/frontend/web/css/toolbar/_findings.css index 9ec4e41..bc23753 100644 --- a/src/view/frontend/web/css/toolbar/_findings.css +++ b/src/view/frontend/web/css/toolbar/_findings.css @@ -67,6 +67,20 @@ background: var(--mageforge-surface-glass-hover); } +.mageforge-findings-toggle:focus-visible { + outline: 2px solid var(--mageforge-color-blue); + outline-offset: 2px; +} + +.mageforge-findings-chevron { + flex-shrink: 0; + transition: transform 0.2s ease; +} + +.mageforge-audit-findings.mageforge-findings-open .mageforge-findings-chevron { + transform: rotate(180deg); +} + /* ── Findings list (hidden until open) ── */ .mageforge-findings-list { diff --git a/src/view/frontend/web/js/toolbar/audits.js b/src/view/frontend/web/js/toolbar/audits.js index 1a889a6..bc2bd01 100644 --- a/src/view/frontend/web/js/toolbar/audits.js +++ b/src/view/frontend/web/js/toolbar/audits.js @@ -47,6 +47,7 @@ export const auditMethods = { * an overall health score (0–100) in the footer gauge. */ async runAllAuditsForScore() { + if (this._batchRunning) return; const btn = this.runAllButton; if (!btn) return; btn.disabled = true; @@ -137,6 +138,7 @@ export const auditMethods = { * compute and display a score (0–100) in that panel's ring. */ async runGroupAuditsForScore(groupKey) { + if (this._batchRunning) return; const btn = this[`runGroupButton-${groupKey}`]; if (!btn) return; btn.disabled = true; diff --git a/src/view/frontend/web/js/toolbar/ui.js b/src/view/frontend/web/js/toolbar/ui.js index 17d7574..00eca13 100644 --- a/src/view/frontend/web/js/toolbar/ui.js +++ b/src/view/frontend/web/js/toolbar/ui.js @@ -885,13 +885,18 @@ export const uiMethods = { const toggleBtn = document.createElement("button"); toggleBtn.type = "button"; toggleBtn.className = "mageforge-findings-toggle"; - toggleBtn.textContent = `Show affected elements (${findings.length})`; + toggleBtn.innerHTML = ` + + Show affected elements (${findings.length}) + `; toggleBtn.addEventListener("click", (e) => { e.stopPropagation(); const isOpen = container.classList.toggle("mageforge-findings-open"); - toggleBtn.textContent = isOpen - ? `Hide affected elements (${findings.length})` - : `Show affected elements (${findings.length})`; + const textEl = toggleBtn.querySelector(".mageforge-findings-toggle-text"); + if (textEl) + textEl.textContent = isOpen + ? `Hide affected elements (${findings.length})` + : `Show affected elements (${findings.length})`; }); container.appendChild(toggleBtn); From 9546d998079aa32561ada9f6911c525d25ea3111 Mon Sep 17 00:00:00 2001 From: Mathias Elle Date: Mon, 22 Jun 2026 12:22:26 +0200 Subject: [PATCH 21/48] feat: Implement audit scoring calculation and improve focus management in toolbar --- src/view/frontend/web/js/toolbar/audits.js | 123 +++++++-------------- src/view/frontend/web/js/toolbar/ui.js | 80 ++++++++++++-- 2 files changed, 110 insertions(+), 93 deletions(-) diff --git a/src/view/frontend/web/js/toolbar/audits.js b/src/view/frontend/web/js/toolbar/audits.js index bc2bd01..9a3d672 100644 --- a/src/view/frontend/web/js/toolbar/audits.js +++ b/src/view/frontend/web/js/toolbar/audits.js @@ -4,6 +4,8 @@ import { audits, auditGroups } from "./audits/index.js"; +const AUDIT_SETTLE_DELAY_MS = 200; + export const auditMethods = { /** * Toggles an audit on/off and updates the menu item state. @@ -42,6 +44,37 @@ export const auditMethods = { } }, + /** + * Calculate a 0–100 score for the given audit list based on current DOM state. + * + * @param {import('./audits/index.js').AuditDefinition[]} auditList + * @returns {number} + */ + _calcScore(auditList) { + let total = 0; + let max = 0; + auditList.forEach((audit) => { + const item = this.menu?.querySelector( + `[data-audit-key="${audit.key}"]`, + ); + if (!item) return; + max += 100; + const status = item.querySelector(".mageforge-toolbar-menu-status"); + if (!status || !status.textContent.trim()) { + total += 100; + } else if ( + status.classList.contains("mageforge-toolbar-menu-status--success") + ) { + total += 100; + } else if ( + status.classList.contains("mageforge-toolbar-menu-status--warning") + ) { + total += 50; + } + }); + return max > 0 ? Math.round((total / max) * 100) : 100; + }, + /** * Run every audit, wait for the DOM to settle, then compute and display * an overall health score (0–100) in the footer gauge. @@ -64,65 +97,17 @@ export const auditMethods = { }); // Allow async DOM mutations to settle - await new Promise((resolve) => setTimeout(resolve, 200)); + await new Promise((resolve) => setTimeout(resolve, AUDIT_SETTLE_DELAY_MS)); - let totalPoints = 0; - let maxPoints = 0; - audits.forEach((audit) => { - const item = this.menu?.querySelector( - `[data-audit-key="${audit.key}"]`, - ); - if (!item) return; - maxPoints += 100; - const status = item.querySelector(".mageforge-toolbar-menu-status"); - if (!status || !status.textContent.trim()) { - totalPoints += 100; - } else if ( - status.classList.contains("mageforge-toolbar-menu-status--success") - ) { - totalPoints += 100; - } else if ( - status.classList.contains("mageforge-toolbar-menu-status--warning") - ) { - totalPoints += 50; - } - // error = 0 points (default) - }); - - const score = - maxPoints > 0 ? Math.round((totalPoints / maxPoints) * 100) : 100; - this.updateHealthScore(score); + this.updateHealthScore(this._calcScore(audits)); // Update per-group scores on the dashboard - const groupScores = {}; - audits.forEach((audit) => { - if (!audit.group) return; - const item = this.menu?.querySelector( - `[data-audit-key="${audit.key}"]`, - ); - if (!item) return; - if (!groupScores[audit.group]) { - groupScores[audit.group] = { total: 0, max: 0 }; - } - groupScores[audit.group].max += 100; - const status = item.querySelector(".mageforge-toolbar-menu-status"); - if (!status || !status.textContent.trim()) { - groupScores[audit.group].total += 100; - } else if ( - status.classList.contains("mageforge-toolbar-menu-status--success") - ) { - groupScores[audit.group].total += 100; - } else if ( - status.classList.contains("mageforge-toolbar-menu-status--warning") - ) { - groupScores[audit.group].total += 50; - } + const grouped = {}; + audits.forEach((a) => { + if (a.group) (grouped[a.group] = grouped[a.group] ?? []).push(a); }); - Object.entries(groupScores).forEach(([groupKey, { total, max }]) => { - this.updateGroupScore( - groupKey, - max > 0 ? Math.round((total / max) * 100) : 100, - ); + Object.entries(grouped).forEach(([groupKey, groupAudits]) => { + this.updateGroupScore(groupKey, this._calcScore(groupAudits)); }); this.updateHomeSummary(); @@ -164,33 +149,9 @@ export const auditMethods = { }); // Allow async DOM mutations to settle - await new Promise((resolve) => setTimeout(resolve, 200)); - - let totalPoints = 0; - let maxPoints = 0; - groupAudits.forEach((audit) => { - const item = this.menu?.querySelector( - `[data-audit-key="${audit.key}"]`, - ); - if (!item) return; - maxPoints += 100; - const status = item.querySelector(".mageforge-toolbar-menu-status"); - if (!status || !status.textContent.trim()) { - totalPoints += 100; - } else if ( - status.classList.contains("mageforge-toolbar-menu-status--success") - ) { - totalPoints += 100; - } else if ( - status.classList.contains("mageforge-toolbar-menu-status--warning") - ) { - totalPoints += 50; - } - }); + await new Promise((resolve) => setTimeout(resolve, AUDIT_SETTLE_DELAY_MS)); - const score = - maxPoints > 0 ? Math.round((totalPoints / maxPoints) * 100) : 100; - this.updateGroupScore(groupKey, score); + this.updateGroupScore(groupKey, this._calcScore(groupAudits)); this.updateHomeSummary(); } finally { this._batchRunning = false; diff --git a/src/view/frontend/web/js/toolbar/ui.js b/src/view/frontend/web/js/toolbar/ui.js index 00eca13..d60c2bf 100644 --- a/src/view/frontend/web/js/toolbar/ui.js +++ b/src/view/frontend/web/js/toolbar/ui.js @@ -44,6 +44,9 @@ const GROUP_ICONS = { // ── Module-level helpers ─────────────────────────────────────────────────── +const GAUGE_ARC_LENGTH = 157.08; // π × r(50) — half-arc stroke length +const SCORE_RING_CIRCUMFERENCE = 113.1; // 2π × r(18) — ring stroke length + function createLogoSvg(fill) { return ``; } @@ -528,7 +531,6 @@ export const uiMethods = { panel.className = "mageforge-home-panel"; if (showHealthScore) { - const ARC_LENGTH = 157.08; const gradId = `mf-gauge-${Math.random().toString(36).slice(2, 8)}`; panel.innerHTML = `
@@ -544,7 +546,7 @@ export const uiMethods = { fill="none" stroke="rgba(148,163,184,0.15)" stroke-width="10" stroke-linecap="round"> + stroke-dasharray="0 ${GAUGE_ARC_LENGTH}" class="mageforge-health-gauge-progress"> @@ -1079,8 +1081,6 @@ export const uiMethods = { */ updateHealthScore(score) { if (!this.menu) return; - const ARC_LENGTH = 157.08; - const CIRCUMFERENCE = 113.1; // Half-arc gauge in the Home panel const progress = this.menu.querySelector( @@ -1090,7 +1090,7 @@ export const uiMethods = { if (progress) progress.setAttribute( "stroke-dasharray", - `${((score / 100) * ARC_LENGTH).toFixed(2)} ${ARC_LENGTH}`, + `${((score / 100) * GAUGE_ARC_LENGTH).toFixed(2)} ${GAUGE_ARC_LENGTH}`, ); if (needle) { const rad = (1 - score / 100) * Math.PI; @@ -1108,7 +1108,7 @@ export const uiMethods = { this.menu.querySelectorAll(".mageforge-score-ring").forEach((ring) => { ring.setAttribute( "stroke-dasharray", - `${((score / 100) * CIRCUMFERENCE).toFixed(2)} ${CIRCUMFERENCE}`, + `${((score / 100) * SCORE_RING_CIRCUMFERENCE).toFixed(2)} ${SCORE_RING_CIRCUMFERENCE}`, ); }); this.menu.querySelectorAll(".mageforge-score-number").forEach((el) => { @@ -1124,7 +1124,6 @@ export const uiMethods = { */ updateGroupScore(groupKey, score) { if (!this.menu) return; - const CIRCUMFERENCE = 113.1; const panel = this.menu.querySelector(`[data-panel="${groupKey}"]`); if (!panel) return; @@ -1133,7 +1132,7 @@ export const uiMethods = { if (ring) { ring.setAttribute( "stroke-dasharray", - `${((score / 100) * CIRCUMFERENCE).toFixed(2)} ${CIRCUMFERENCE}`, + `${((score / 100) * SCORE_RING_CIRCUMFERENCE).toFixed(2)} ${SCORE_RING_CIRCUMFERENCE}`, ); } const number = panel.querySelector(".mageforge-score-number"); @@ -1160,14 +1159,12 @@ export const uiMethods = { resetScore() { this.deactivateAllAudits(); if (!this.menu) return; - const ARC_LENGTH = 157.08; - const CIRCUMFERENCE = 113.1; const progress = this.menu.querySelector( ".mageforge-health-gauge-progress", ); const needle = this.menu.querySelector(".mageforge-health-gauge-needle"); - if (progress) progress.setAttribute("stroke-dasharray", `0 ${ARC_LENGTH}`); + if (progress) progress.setAttribute("stroke-dasharray", `0 ${GAUGE_ARC_LENGTH}`); if (needle) needle.setAttribute("opacity", "0"); this.menu .querySelectorAll(".mageforge-toolbar-health-score-number") @@ -1175,7 +1172,7 @@ export const uiMethods = { el.textContent = "--"; }); this.menu.querySelectorAll(".mageforge-score-ring").forEach((ring) => { - ring.setAttribute("stroke-dasharray", `0 ${CIRCUMFERENCE}`); + ring.setAttribute("stroke-dasharray", `0 ${SCORE_RING_CIRCUMFERENCE}`); }); this.menu.querySelectorAll(".mageforge-score-number").forEach((el) => { el.textContent = "--"; @@ -1209,6 +1206,7 @@ export const uiMethods = { this.menu.classList.add("mageforge-menu-open"); this.burgerButton.classList.add("mageforge-active"); this.burgerButton.setAttribute("aria-expanded", "true"); + this._trapFocus(); }, closeMenu() { @@ -1216,6 +1214,63 @@ export const uiMethods = { this.menu.classList.remove("mageforge-menu-open"); this.burgerButton.classList.remove("mageforge-active"); this.burgerButton.setAttribute("aria-expanded", "false"); + this._releaseFocusTrap(); + this.burgerButton?.focus(); + }, + + /** + * Returns all currently focusable elements within the open menu. + * + * @returns {HTMLElement[]} + */ + _getFocusableEls() { + return Array.from( + this.menu.querySelectorAll( + 'button:not([disabled]), [href], input:not([disabled]), [tabindex]:not([tabindex="-1"])', + ), + ).filter((el) => !el.closest("[hidden]")); + }, + + /** + * Trap keyboard focus inside the menu and close on Escape. + */ + _trapFocus() { + this._focusTrapHandler = (e) => { + if (e.key === "Escape") { + e.preventDefault(); + this.closeMenu(); + return; + } + if (e.key !== "Tab") return; + const focusable = this._getFocusableEls(); + if (!focusable.length) return; + const first = focusable[0]; + const last = focusable[focusable.length - 1]; + if (e.shiftKey) { + if (document.activeElement === first) { + e.preventDefault(); + last.focus(); + } + } else { + if (document.activeElement === last) { + e.preventDefault(); + first.focus(); + } + } + }; + this.menu.addEventListener("keydown", this._focusTrapHandler); + const focusable = this._getFocusableEls(); + if (focusable.length) focusable[0].focus(); + }, + + /** + * Remove the focus trap listener and clean up. + */ + _releaseFocusTrap() { + if (this._focusTrapHandler) { + this.menu?.removeEventListener("keydown", this._focusTrapHandler); + this._focusTrapHandler = null; + } }, destroyToolbar() { @@ -1223,6 +1278,7 @@ export const uiMethods = { document.removeEventListener("click", this._outsideClickHandler); this._outsideClickHandler = null; } + this._releaseFocusTrap(); if (this.container?.parentNode) this.container.parentNode.removeChild(this.container); this.container = null; From 7f7639fa2dfff5cd27acb8b8a827a1ec1f57b823 Mon Sep 17 00:00:00 2001 From: Mathias Elle Date: Mon, 22 Jun 2026 13:19:19 +0200 Subject: [PATCH 22/48] feat: Redesign toolbar layout and enhance responsive styles --- .../frontend/web/css/toolbar/_responsive.css | 134 +++++++++++++++++- 1 file changed, 127 insertions(+), 7 deletions(-) diff --git a/src/view/frontend/web/css/toolbar/_responsive.css b/src/view/frontend/web/css/toolbar/_responsive.css index 744475e..51ea5ec 100644 --- a/src/view/frontend/web/css/toolbar/_responsive.css +++ b/src/view/frontend/web/css/toolbar/_responsive.css @@ -8,41 +8,161 @@ */ @media (max-width: 640px) { + /* ── Toolbar position ──────────────────────────────────────────────── */ + .mageforge-toolbar { bottom: 10px; left: 10px; } + .mageforge-toolbar[data-position="bottom-right"] { left: auto; right: 10px; } + .mageforge-toolbar[data-position="top-left"] { bottom: auto; left: 10px; top: 10px; } + .mageforge-toolbar[data-position="top-right"] { bottom: auto; left: auto; right: 10px; top: 10px; } + + /* ── Menu: fixed overlay, fills viewport width with 10px margins ─── */ + .mageforge-toolbar-menu { - min-width: 320px; - max-height: calc(100vh - 60px); + position: fixed; + left: 10px; + right: 10px; + bottom: 62px; /* clear the burger button row */ + width: auto; + min-width: unset; + min-height: unset; + max-height: calc(100dvh - 90px); + z-index: 9999998; + border-radius: 8px; } + + /* For top-positioned toolbars the menu opens downward */ + .mageforge-toolbar[data-position^="top"] .mageforge-toolbar-menu { + bottom: auto; + top: 62px; + } + + /* ── Title bar ─────────────────────────────────────────────────────── */ + + .mageforge-toolbar-menu-title { + padding: 8px 10px 6px; + } + + .mageforge-toolbar-menu-title-text { + font-size: 16px; + } + + /* ── Icon-only left nav sidebar ────────────────────────────────────── */ + .mageforge-toolbar-tab-nav { - width: 60px; + width: 52px; + padding: 4px 2px; + } + + .mageforge-toolbar-tab-btn { + flex-direction: column; + align-items: center; + justify-content: center; + gap: 2px; + padding: 10px 4px; + } + + /* Hide text labels – icons are self-explanatory in icon-only mode */ + .mageforge-tab-label { + display: none; + } + + /* Hide count badges – too cramped at 52 px nav width */ + .mageforge-tab-badges { + display: none; } - .mageforge-tab-icon { - width: 36px; - height: 30px; + + /* ── Nav action bar: icon-only run / reset buttons ─────────────────── */ + + .mageforge-nav-action-bar { + padding: 8px 4px 4px; + } + + .mageforge-nav-action-bar .mageforge-footer-btn-row { + flex-direction: column; + gap: 6px; + } + + /* Collapse text to 0, show only the SVG icon */ + .mageforge-nav-action-bar .mageforge-group-run-btn { + font-size: 0; + letter-spacing: 0; + line-height: 0; + padding: 10px; + justify-content: center; + gap: 0; + } + + .mageforge-nav-action-bar .mageforge-group-run-btn svg { + width: 18px; + height: 18px; + flex-shrink: 0; + } + + /* Keep spinner visible in running state */ + .mageforge-nav-action-bar .mageforge-group-run-btn.mageforge-running svg { + display: none; + } + + .mageforge-nav-action-bar .mageforge-group-reset-btn { + font-size: 0; + letter-spacing: 0; + line-height: 1; + padding: 8px; + justify-content: center; + gap: 0; + width: auto; + height: auto; } + + .mageforge-nav-action-bar .mageforge-group-reset-btn svg { + width: 16px; + height: 16px; + flex-shrink: 0; + } + + /* ── Burger button ─────────────────────────────────────────────────── */ + .mageforge-toolbar-burger-label { display: none; } + .mageforge-toolbar-burger { - height: 32px; + height: 36px; width: auto; } + + /* ── Content panels ────────────────────────────────────────────────── */ + + .mageforge-tab-panel-body { + max-height: calc(100dvh - 230px); + } + + /* ── Dashboard: tighter gauge ──────────────────────────────────────── */ + + .mageforge-toolbar-health-gauge { + width: 110px; + height: 65px; + } + + .mageforge-toolbar-health-score-value { + font-size: 16px; + } } From 2cce727ea4548beab1b12571400532f933cd04ec Mon Sep 17 00:00:00 2001 From: Mathias Elle Date: Mon, 22 Jun 2026 13:52:16 +0200 Subject: [PATCH 23/48] feat: Add SEO audit features and enhance toolbar styles --- src/view/frontend/web/css/toolbar/_groups.css | 12 +++- src/view/frontend/web/css/toolbar/_menu.css | 19 ++++++- .../frontend/web/css/toolbar/_variables.css | 1 + .../frontend/web/js/toolbar/audits/index.js | 19 +++++++ .../toolbar/audits/inline-event-handlers.js | 57 +++++++++++++++++++ .../js/toolbar/audits/nested-interactive.js | 36 ++++++++++++ .../toolbar/audits/render-blocking-scripts.js | 35 ++++++++++++ .../toolbar/audits/seo-heading-hierarchy.js | 48 ++++++++++++++++ .../toolbar/audits/seo-missing-canonical.js | 28 +++++++++ .../js/toolbar/audits/seo-missing-json-ld.js | 28 +++++++++ .../web/js/toolbar/audits/seo-missing-lang.js | 28 +++++++++ .../audits/seo-missing-meta-description.js | 27 +++++++++ .../js/toolbar/audits/seo-missing-title.js | 27 +++++++++ src/view/frontend/web/js/toolbar/ui.js | 37 ++++++++++-- 14 files changed, 394 insertions(+), 8 deletions(-) create mode 100644 src/view/frontend/web/js/toolbar/audits/inline-event-handlers.js create mode 100644 src/view/frontend/web/js/toolbar/audits/nested-interactive.js create mode 100644 src/view/frontend/web/js/toolbar/audits/render-blocking-scripts.js create mode 100644 src/view/frontend/web/js/toolbar/audits/seo-heading-hierarchy.js create mode 100644 src/view/frontend/web/js/toolbar/audits/seo-missing-canonical.js create mode 100644 src/view/frontend/web/js/toolbar/audits/seo-missing-json-ld.js create mode 100644 src/view/frontend/web/js/toolbar/audits/seo-missing-lang.js create mode 100644 src/view/frontend/web/js/toolbar/audits/seo-missing-meta-description.js create mode 100644 src/view/frontend/web/js/toolbar/audits/seo-missing-title.js diff --git a/src/view/frontend/web/css/toolbar/_groups.css b/src/view/frontend/web/css/toolbar/_groups.css index 732ebce..86ea976 100644 --- a/src/view/frontend/web/css/toolbar/_groups.css +++ b/src/view/frontend/web/css/toolbar/_groups.css @@ -95,6 +95,11 @@ background: rgba(251, 146, 60, 0.1); } +.mageforge-toolbar-tab-btn[data-tab="seo"].mageforge-tab-active { + color: var(--mageforge-group-color-seo); + background: rgba(20, 184, 166, 0.1); +} + /* Left-edge indicator bar on active tab */ .mageforge-toolbar-tab-btn.mageforge-tab-active::before { @@ -130,7 +135,6 @@ display: flex; align-items: center; gap: 4px; - margin-top: 4px; justify-self: center; } @@ -138,6 +142,7 @@ display: none; align-items: center; gap: 2px; + margin-top: -2px; font-size: 10px; font-weight: 700; line-height: 1; @@ -377,3 +382,8 @@ .mageforge-toolbar-menu-icon { color: var(--mageforge-group-color-performance); } + +.mageforge-toolbar-menu-item[data-group-key="seo"] + .mageforge-toolbar-menu-icon { + color: var(--mageforge-group-color-seo); +} diff --git a/src/view/frontend/web/css/toolbar/_menu.css b/src/view/frontend/web/css/toolbar/_menu.css index 7e003b4..963a487 100644 --- a/src/view/frontend/web/css/toolbar/_menu.css +++ b/src/view/frontend/web/css/toolbar/_menu.css @@ -245,6 +245,16 @@ background: var(--mageforge-color-green); } +.mageforge-toolbar-menu-item.mageforge-active.mageforge-active--error + .mageforge-toolbar-menu-toggle { + background: var(--mageforge-color-red); +} + +.mageforge-toolbar-menu-item.mageforge-active.mageforge-active--warning + .mageforge-toolbar-menu-toggle { + background: var(--mageforge-color-amber); +} + .mageforge-toolbar-menu-item.mageforge-active .mageforge-toolbar-menu-toggle::after { transform: translateX(12px); @@ -305,15 +315,18 @@ .mageforge-toolbar-menu-status--success { color: var(--mageforge-color-green); - background: transparent; + background: rgba(16, 185, 129, 0.15); + border: 1px solid rgba(16, 185, 129, 0.3); } .mageforge-toolbar-menu-status--error { color: var(--mageforge-color-red); - background: transparent; + background: rgba(239, 68, 68, 0.15); + border: 1px solid rgba(239, 68, 68, 0.3); } .mageforge-toolbar-menu-status--warning { color: var(--mageforge-color-amber); - background: transparent; + background: rgba(245, 158, 11, 0.15); + border: 1px solid rgba(245, 158, 11, 0.3); } diff --git a/src/view/frontend/web/css/toolbar/_variables.css b/src/view/frontend/web/css/toolbar/_variables.css index 4907c48..1214141 100644 --- a/src/view/frontend/web/css/toolbar/_variables.css +++ b/src/view/frontend/web/css/toolbar/_variables.css @@ -36,6 +36,7 @@ --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-group-color-seo: #14b8a6; /* Backgrounds */ --mageforge-bg-dark: rgba(15, 23, 42, 0.98); diff --git a/src/view/frontend/web/js/toolbar/audits/index.js b/src/view/frontend/web/js/toolbar/audits/index.js index 777ef97..0414789 100644 --- a/src/view/frontend/web/js/toolbar/audits/index.js +++ b/src/view/frontend/web/js/toolbar/audits/index.js @@ -28,9 +28,18 @@ import emptyInteractive from "./empty-interactive.js"; import imagesWithoutAlt from "./images-without-alt.js"; import imagesWithoutDimensions from "./images-without-dimensions.js"; import imagesWithoutLazyLoad from "./images-without-lazy-load.js"; +import inlineEventHandlers from "./inline-event-handlers.js"; import inputsWithoutLabel from "./inputs-without-label.js"; import lowContrastText from "./low-contrast-text.js"; import multipleH1 from "./multiple-h1.js"; +import nestedInteractive from "./nested-interactive.js"; +import renderBlockingScripts from "./render-blocking-scripts.js"; +import seoHeadingHierarchy from "./seo-heading-hierarchy.js"; +import seoMissingCanonical from "./seo-missing-canonical.js"; +import seoMissingJsonLd from "./seo-missing-json-ld.js"; +import seoMissingLang from "./seo-missing-lang.js"; +import seoMissingMetaDescription from "./seo-missing-meta-description.js"; +import seoMissingTitle from "./seo-missing-title.js"; import smallTouchTargets from "./small-touch-targets.js"; import tabOrder from "./tab-order.js"; import unsafeBlankTarget from "./unsafe-blank-target.js"; @@ -40,6 +49,7 @@ export const auditGroups = [ { key: "html-quality", label: "HTML Quality" }, { key: "wcag", label: "Accessibility" }, { key: "performance", label: "Performance" }, + { key: "seo", label: "SEO" }, ]; /** @type {AuditDefinition[]} */ @@ -54,6 +64,15 @@ export const audits = [ { ...smallTouchTargets, group: "wcag" }, { ...duplicateIds, group: "html-quality" }, { ...unsafeBlankTarget, group: "html-quality" }, + { ...inlineEventHandlers, group: "html-quality" }, + { ...nestedInteractive, group: "html-quality" }, { ...imagesWithoutDimensions, group: "performance" }, { ...imagesWithoutLazyLoad, group: "performance" }, + { ...renderBlockingScripts, group: "performance" }, + { ...seoMissingTitle, group: "seo" }, + { ...seoMissingMetaDescription, group: "seo" }, + { ...seoMissingCanonical, group: "seo" }, + { ...seoMissingLang, group: "seo" }, + { ...seoHeadingHierarchy, group: "seo" }, + { ...seoMissingJsonLd, group: "seo" }, ]; diff --git a/src/view/frontend/web/js/toolbar/audits/inline-event-handlers.js b/src/view/frontend/web/js/toolbar/audits/inline-event-handlers.js new file mode 100644 index 0000000..3460b54 --- /dev/null +++ b/src/view/frontend/web/js/toolbar/audits/inline-event-handlers.js @@ -0,0 +1,57 @@ +/** + * MageForge Toolbar Audit – Inline event handler attributes + * + * Inline event handlers (onclick, onchange, etc.) violate Content Security + * Policy. On Magento storefronts that enforce a CSP header, they are blocked + * silently and cause broken functionality that is hard to debug. + * + * Icon source: Tabler Icons (MIT) + */ + +import { createAudit } from "./createAudit.js"; + +const EVENT_ATTRS = [ + "onclick", + "ondblclick", + "onmousedown", + "onmouseup", + "onmouseover", + "onmouseout", + "onmousemove", + "onkeydown", + "onkeyup", + "onkeypress", + "onchange", + "oninput", + "onfocus", + "onblur", + "onsubmit", + "onreset", + "onselect", + "onload", + "onerror", +]; + +const SELECTOR = EVENT_ATTRS.map((a) => `[${a}]`).join(", "); + +export default createAudit( + { + key: "inline-event-handlers", + icon: '', + label: "Inline Event Handlers", + description: "Highlights elements with inline event handler attributes (CSP risk)", + }, + () => { + return Array.from(document.querySelectorAll(SELECTOR)).filter((el) => { + if (el.closest(".mageforge-toolbar")) return false; + if (!el.offsetParent && getComputedStyle(el).position !== "fixed") + return false; + const style = getComputedStyle(el); + return ( + style.visibility !== "hidden" && + style.display !== "none" && + parseFloat(style.opacity) !== 0 + ); + }); + }, +); diff --git a/src/view/frontend/web/js/toolbar/audits/nested-interactive.js b/src/view/frontend/web/js/toolbar/audits/nested-interactive.js new file mode 100644 index 0000000..da0049b --- /dev/null +++ b/src/view/frontend/web/js/toolbar/audits/nested-interactive.js @@ -0,0 +1,36 @@ +/** + * MageForge Toolbar Audit – Nested interactive elements + * + * Placing interactive elements inside each other ( in ,