/** * Grav Problems Report — Web Component for admin-next reports. * * Receives `report` property with structured problem data from the API. * Each item in report.items is a Problem: { id, status, level, msg, help, details } * * Uses CSS custom properties from the admin-next theme for light/dark mode support. */ const TAG = window.__GRAV_REPORT_TAG || 'grav-problems--problems-report'; class ProblemsReportElement extends HTMLElement { #report = null; set report(val) { this.#report = val; this.render(); } get report() { return this.#report; } connectedCallback() { if (this.#report) this.render(); } render() { const report = this.#report; if (!report) return; const items = report.items || []; const style = document.createElement('style'); style.textContent = ` :host { display: block; font-family: inherit; } .status-bar { display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 8px 16px; font-size: 13px; font-weight: 500; border-bottom: 1px solid var(--border, #e5e7eb); } .status-bar.success { background: color-mix(in srgb, #22c55e 12%, transparent); color: color-mix(in srgb, #16a34a 80%, var(--foreground, #1f2937)); } .status-bar.error { background: color-mix(in srgb, #ef4444 12%, transparent); color: color-mix(in srgb, #dc2626 80%, var(--foreground, #1f2937)); } .status-bar.warning { background: color-mix(in srgb, #a78bfa 12%, transparent); color: color-mix(in srgb, #7c3aed 70%, var(--foreground, #1f2937)); } .status-bar .msg { flex: 1; } .status-bar .msg strong { font-weight: 700; } .help-link { display: inline-flex; align-items: center; gap: 4px; color: var(--muted-foreground, #6b7280); text-decoration: none; font-size: 11px; font-weight: 500; padding: 2px 8px; border-radius: 4px; border: 1px solid var(--border, #e5e7eb); background: var(--card, #fff); white-space: nowrap; transition: border-color 0.15s; } .help-link:hover { border-color: var(--foreground, #1f2937); color: var(--foreground, #1f2937); } .help-link svg { width: 12px; height: 12px; } .detail-list { border-top: none; } .detail-item { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 7px 16px; font-size: 13px; color: var(--foreground, #1f2937); border-bottom: 1px solid var(--border, #e5e7eb); } .detail-item:last-child { border-bottom: none; } .detail-msg { flex: 1; min-width: 0; color: var(--muted-foreground, #6b7280); } .detail-msg .module-name { font-weight: 600; margin-right: 2px; } .detail-msg .module-name.error-name { color: color-mix(in srgb, #ef4444 85%, var(--foreground, #1f2937)); } .detail-msg .module-name.warning-name { color: var(--primary, #3b82f6); } .detail-msg .module-name.success-name { color: color-mix(in srgb, #22c55e 85%, var(--foreground, #1f2937)); } .status-icon { flex-shrink: 0; width: 18px; height: 18px; border-radius: 3px; display: flex; align-items: center; justify-content: center; } .status-icon svg { width: 12px; height: 12px; } .status-icon.success-icon { background: color-mix(in srgb, #22c55e 15%, transparent); color: color-mix(in srgb, #22c55e 85%, var(--foreground, #1f2937)); } .status-icon.warning-icon { background: color-mix(in srgb, var(--primary, #3b82f6) 15%, transparent); color: var(--primary, #3b82f6); } .status-icon.error-icon { background: color-mix(in srgb, #ef4444 15%, transparent); color: color-mix(in srgb, #ef4444 85%, var(--foreground, #1f2937)); } `; const shadow = this.shadowRoot || this.attachShadow({ mode: 'open' }); shadow.innerHTML = ''; shadow.appendChild(style); for (const item of items) { const section = document.createElement('div'); section.className = 'problem-section'; // Determine bar color let barClass = 'success'; if (!item.status && item.level === 'critical') barClass = 'error'; else if (!item.status && item.level === 'warning') barClass = 'warning'; else if (item.status && this.hasWarnings(item)) barClass = 'warning'; // Status bar const bar = document.createElement('div'); bar.className = `status-bar ${barClass}`; const msgSpan = document.createElement('span'); msgSpan.className = 'msg'; msgSpan.innerHTML = `${this.escHtml(item.id)}: ${item.msg}`; bar.appendChild(msgSpan); if (item.help) { const helpLink = document.createElement('a'); helpLink.className = 'help-link'; helpLink.href = item.help; helpLink.target = '_blank'; helpLink.rel = 'noopener'; helpLink.innerHTML = ` Help`; bar.appendChild(helpLink); } section.appendChild(bar); // Detail items (errors, warnings, success) if (item.details && typeof item.details === 'object') { const detailList = document.createElement('div'); detailList.className = 'detail-list'; this.renderDetails(detailList, item.details.errors, 'error'); this.renderDetails(detailList, item.details.warning, 'warning'); this.renderDetails(detailList, item.details.success, 'success'); if (detailList.children.length > 0) { section.appendChild(detailList); } } shadow.appendChild(section); } } hasWarnings(item) { return item.details?.warning && Object.keys(item.details.warning).length > 0; } renderDetails(container, details, type) { if (!details || typeof details !== 'object') return; for (const [module, message] of Object.entries(details)) { const row = document.createElement('div'); row.className = 'detail-item'; const msgEl = document.createElement('span'); msgEl.className = 'detail-msg'; msgEl.innerHTML = `${this.escHtml(module)} - ${this.escHtml(message)}`; row.appendChild(msgEl); const icon = document.createElement('span'); icon.className = `status-icon ${type}-icon`; icon.innerHTML = type === 'success' ? '' : ''; row.appendChild(icon); container.appendChild(row); } } escHtml(str) { if (typeof str !== 'string') return ''; const div = document.createElement('div'); div.textContent = str; return div.innerHTML; } } customElements.define(TAG, ProblemsReportElement);