From 1ab374531c4fe3f893904201168c60c47189addc Mon Sep 17 00:00:00 2001 From: Jared Vititoe Date: Sat, 4 Apr 2026 18:25:27 -0400 Subject: [PATCH] fix: avatar image overlays initials, chart canvas responsive sizing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Avatar bug: - base.css: .lt-avatar now position:relative; img is position:absolute inset:0 so a loaded image covers the initials span (fixes img+initials shown together) - base.css: .lt-avatar img.lt-avatar-img-err { display:none } — CSS hook for error state - layout_footer.php: capture-phase error event delegation on .lt-avatar imgs replaces blocked inline onerror handlers (CSP has no unsafe-inline in script-src) Chart bug: - DashboardView: replaced display:flex section-body containers with a position:relative; width:100%; height:170px div wrapper for each canvas (Chart.js responsive:true reads parentNode dimensions; flex containers give canvas zero intrinsic width causing 0×0 render = empty charts) - Removed has-lt-overlay from chart frames (no overlay div was injected) Co-Authored-By: Claude Sonnet 4.6 --- assets/css/base.css | 13 ++++++++++++- views/DashboardView.php | 30 +++++++++++++++--------------- views/layout_footer.php | 10 ++++++++++ 3 files changed, 37 insertions(+), 16 deletions(-) diff --git a/assets/css/base.css b/assets/css/base.css index d3c5c9b..11363b2 100644 --- a/assets/css/base.css +++ b/assets/css/base.css @@ -4490,8 +4490,19 @@ body.lt-is-offline .lt-main { margin-top: 2rem; transition: margin-top 0.25s eas overflow: hidden; flex-shrink: 0; user-select: none; + position: relative; /* needed so img can overlay initials absolutely */ } -.lt-avatar img { width: 100%; height: 100%; object-fit: cover; display: block; } +/* Image overlays initials — hidden by JS (.lt-avatar-img-err) when broken */ +.lt-avatar img { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + object-fit: cover; + display: block; +} +/* When the image fails, JS adds .lt-avatar-img-err to hide it, revealing initials */ +.lt-avatar img.lt-avatar-img-err { display: none; } /* Sizes */ .lt-avatar--xs { width: 1.5rem; height: 1.5rem; font-size: 0.55rem; } .lt-avatar--sm { width: 2rem; height: 2rem; font-size: 0.65rem; } diff --git a/views/DashboardView.php b/views/DashboardView.php index 7cdc52a..ca91942 100644 --- a/views/DashboardView.php +++ b/views/DashboardView.php @@ -177,25 +177,31 @@ include __DIR__ . '/layout_header.php'; CHARTS ROW (Chart.js — loaded from CDN on this page only) ═══════════════════════════════════════════════════════════ -->
-
+
Priority Distribution
-
- +
+
+ +
-
+
Status Breakdown
-
- +
+
+ +
-
+
Category Breakdown
-
- +
+
+ +
@@ -289,12 +295,6 @@ include __DIR__ . '/layout_header.php'; var pColors = { 'P1': COLORS.red, 'P2': COLORS.amber, 'P3': COLORS.cyan, 'P4': COLORS.green, 'P5': COLORS.muted }; var sColors = { 'Open': COLORS.green, 'Pending': COLORS.amber, 'In Progress': COLORS.cyan, 'Closed': COLORS.muted }; - // Remove loading overlays - ['chartPriorityWrap','chartStatusWrap','chartCategoryWrap'].forEach(function(id) { - var el = document.getElementById(id); - if (el) { el.classList.remove('has-lt-overlay'); var ov = el.querySelector('.lt-loading-overlay'); if (ov) ov.remove(); } - }); - makeDonut('chartPriority', priorityData, pColors); makeDonut('chartStatus', statusData, sColors); makeBar('chartCategory', categoryData.slice(0, 8)); diff --git a/views/layout_footer.php b/views/layout_footer.php index bb7ec4b..15eb56b 100644 --- a/views/layout_footer.php +++ b/views/layout_footer.php @@ -250,6 +250,16 @@ })(); + // ── Avatar image error fallback (CSP blocks inline onerror) ────── + // Uses capture-phase error delegation: if an img inside .lt-avatar + // fails to load, add .lt-avatar-img-err to hide it (CSS display:none), + // revealing the initials span underneath. + document.addEventListener('error', function(e) { + if (e.target.tagName === 'IMG' && e.target.closest('.lt-avatar')) { + e.target.classList.add('lt-avatar-img-err'); + } + }, true); + // Footer hint bar actions (keyboard help + settings — work on all pages) document.addEventListener('click', function(e) { var btn = e.target.closest('[data-action]');