Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 47 additions & 12 deletions app/figma-plugin/src/ui.template.html
Original file line number Diff line number Diff line change
Expand Up @@ -150,25 +150,29 @@ <h2>Ready to analyze</h2>
'<div class="grade-score">' + scores.overall.percentage + ' <span>/ 100</span></div>' +
'<div class="grade-label">Overall Score &middot; ' + result.nodeCount + ' nodes</div>';

// Category grid
// Category list
var categories = CanICode.CATEGORIES || CATEGORIES;
var catTips = { structure: 'Can AI read the layout? (Auto Layout, nesting, positioning)', token: 'Can AI reproduce exact values? (colors, fonts, shadows, spacing)', component: 'Is the design efficient for AI context? (reuse, variants)', naming: 'Can AI infer meaning? (semantic names, conventions)', behavior: 'Can AI know what happens? (overflow, truncation, interactions)' };
var gridEl = document.getElementById('cat-grid');
gridEl.innerHTML = categories.map(function(cat) {
var cs = scores.byCategory[cat];
return '<div class="cat-card" data-cat="' + cat + '" onclick="filterCategory(\'' + cat + '\')">' +
var barColor = cs.percentage >= 80 ? 'var(--green)' : cs.percentage >= 60 ? 'var(--amber)' : 'var(--red)';
return '<div class="cat-row">' +
'<div class="cat-score ' + scoreClass(cs.percentage) + '">' + cs.percentage + '</div>' +
'<div class="cat-label">' + (CATEGORY_LABELS[cat] || cat) + '</div>' +
'<div class="cat-issues">' + cs.issueCount + ' issues</div>' +
'<div class="cat-label" data-tip="' + esc(catTips[cat] || '') + '">' + (CATEGORY_LABELS[cat] || cat) + '</div>' +
'<div class="cat-bar"><div class="cat-bar-fill" style="width:' + cs.percentage + '%;background:' + barColor + '"></div></div>' +
'<div class="cat-issues">' + cs.issueCount + '</div>' +
'</div>';
}).join('');

// Summary bar
var s = scores.summary;
var sevTips = { blocking: 'Cannot implement correctly without fixing. Direct impact on screen reproduction.', risk: 'Implementable now but will break or increase cost later.', missing: 'Information is absent, forcing AI to guess.', suggestion: 'Not immediately problematic, but improves systemization.' };
document.getElementById('summary-bar').innerHTML =
'<div class="summary-item"><span class="dot dot-blocking"></span><span class="summary-count">' + s.blocking + '</span> Blocking</div>' +
'<div class="summary-item"><span class="dot dot-risk"></span><span class="summary-count">' + s.risk + '</span> Risk</div>' +
'<div class="summary-item"><span class="dot dot-missing"></span><span class="summary-count">' + s.missingInfo + '</span> Missing</div>' +
'<div class="summary-item"><span class="dot dot-suggestion"></span><span class="summary-count">' + s.suggestion + '</span> Suggestion</div>' +
'<div class="summary-item" data-tip="' + esc(sevTips.blocking) + '"><span class="dot dot-blocking"></span><span class="summary-count">' + s.blocking + '</span> Blocking</div>' +
'<div class="summary-item" data-tip="' + esc(sevTips.risk) + '"><span class="dot dot-risk"></span><span class="summary-count">' + s.risk + '</span> Risk</div>' +
'<div class="summary-item" data-tip="' + esc(sevTips.missing) + '"><span class="dot dot-missing"></span><span class="summary-count">' + s.missingInfo + '</span> Missing</div>' +
'<div class="summary-item" data-tip="' + esc(sevTips.suggestion) + '"><span class="dot dot-suggestion"></span><span class="summary-count">' + s.suggestion + '</span> Suggestion</div>' +
'<div class="summary-total">' + s.totalIssues + ' total</div>';

// Filter tabs
Expand Down Expand Up @@ -260,10 +264,6 @@ <h2>Ready to analyze</h2>
document.querySelectorAll('.filter-tab').forEach(function(tab) {
tab.classList.toggle('active', tab.dataset.cat === cat);
});
// Update category card highlights
document.querySelectorAll('.cat-card').forEach(function(card) {
card.classList.toggle('active', card.dataset.cat === cat);
});
if (currentResult) {
renderIssues(currentResult.issues, cat);
}
Expand Down Expand Up @@ -343,6 +343,41 @@ <h2>Ready to analyze</h2>
}
}
};

// ---- Tooltip (JS-based, works inside Figma iframe) ----
(function() {
var tip = document.createElement('div');
tip.className = 'cic-tooltip';
var arrow = document.createElement('div');
arrow.className = 'cic-tooltip-arrow';
document.body.appendChild(tip);
document.body.appendChild(arrow);

function hide() { tip.classList.remove('visible'); arrow.classList.remove('visible'); }

document.addEventListener('mouseover', function(e) {
var el = e.target.closest('[data-tip]');
if (!el) { hide(); return; }
tip.textContent = el.getAttribute('data-tip');
tip.classList.add('visible');
var rect = el.getBoundingClientRect();
var tipH = tip.offsetHeight;
var tipW = tip.offsetWidth;
var cx = rect.left + rect.width / 2;
var tipLeft = Math.max(4, Math.min(cx - tipW / 2, window.innerWidth - tipW - 4));
var above = rect.top - tipH - 10 >= 0;
var tipTop = above ? rect.top - tipH - 10 : rect.bottom + 10;
tip.style.left = tipLeft + 'px';
tip.style.top = tipTop + 'px';
arrow.style.left = (cx - 4) + 'px';
arrow.style.top = (above ? rect.top - 14 : rect.bottom + 6) + 'px';
arrow.classList.add('visible');
});
document.addEventListener('mouseleave', hide);
document.addEventListener('mouseout', function(e) {
if (!e.target.closest('[data-tip]')) hide();
});
Comment on lines +356 to +379
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Hide the tooltip when the pointer leaves the plugin.

The current mouseout path only hides after another in-document hover target is processed. If the cursor exits the iframe directly from a [data-tip] element, the last tooltip stays stuck onscreen until the mouse re-enters.

🐛 Suggested fix
-      document.addEventListener('mouseout', function(e) {
-        if (!e.target.closest('[data-tip]')) hide();
-      });
+      document.addEventListener('mouseout', function(e) {
+        var to = e.relatedTarget && e.relatedTarget.closest ? e.relatedTarget.closest('[data-tip]') : null;
+        if (!to) hide();
+      });
+      document.documentElement.addEventListener('mouseleave', hide);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function hide() { tip.classList.remove('visible'); arrow.classList.remove('visible'); }
document.addEventListener('mouseover', function(e) {
var el = e.target.closest('[data-tip]');
if (!el) { hide(); return; }
tip.textContent = el.getAttribute('data-tip');
tip.classList.add('visible');
var rect = el.getBoundingClientRect();
var tipH = tip.offsetHeight;
var tipW = tip.offsetWidth;
var cx = rect.left + rect.width / 2;
var tipLeft = Math.max(4, Math.min(cx - tipW / 2, window.innerWidth - tipW - 4));
var above = rect.top - tipH - 10 >= 0;
var tipTop = above ? rect.top - tipH - 10 : rect.bottom + 10;
tip.style.left = tipLeft + 'px';
tip.style.top = tipTop + 'px';
arrow.style.left = (cx - 4) + 'px';
arrow.style.top = (above ? rect.top - 14 : rect.bottom + 6) + 'px';
arrow.classList.add('visible');
});
document.addEventListener('mouseout', function(e) {
if (!e.target.closest('[data-tip]')) hide();
});
function hide() { tip.classList.remove('visible'); arrow.classList.remove('visible'); }
document.addEventListener('mouseover', function(e) {
var el = e.target.closest('[data-tip]');
if (!el) { hide(); return; }
tip.textContent = el.getAttribute('data-tip');
tip.classList.add('visible');
var rect = el.getBoundingClientRect();
var tipH = tip.offsetHeight;
var tipW = tip.offsetWidth;
var cx = rect.left + rect.width / 2;
var tipLeft = Math.max(4, Math.min(cx - tipW / 2, window.innerWidth - tipW - 4));
var above = rect.top - tipH - 10 >= 0;
var tipTop = above ? rect.top - tipH - 10 : rect.bottom + 10;
tip.style.left = tipLeft + 'px';
tip.style.top = tipTop + 'px';
arrow.style.left = (cx - 4) + 'px';
arrow.style.top = (above ? rect.top - 14 : rect.bottom + 6) + 'px';
arrow.classList.add('visible');
});
document.addEventListener('mouseout', function(e) {
var to = e.relatedTarget && e.relatedTarget.closest ? e.relatedTarget.closest('[data-tip]') : null;
if (!to) hide();
});
document.documentElement.addEventListener('mouseleave', hide);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/figma-plugin/src/ui.template.html` around lines 360 - 382, The tooltip
can stick if the pointer leaves the iframe because the current mouseout handler
only hides when moving to another in-doc element; update the listeners so the
hide() function is also invoked when the pointer leaves the plugin bounds —
e.g., add a document.addEventListener('mouseleave', hide) and/or modify the
existing document.addEventListener('mouseout', ...) to also call hide() when
e.relatedTarget is null (or not inside the iframe) so hide() (the function
defined at the top) always runs when the cursor exits the frame.

})();
</script>
</body>
</html>
83 changes: 65 additions & 18 deletions app/shared/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -55,45 +55,92 @@ body {
margin-top: 2px;
}

/* ---- Category grid ---- */
/* ---- Category list ---- */
.cat-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
margin-bottom: 16px;
}
.cat-card {
display: flex;
flex-direction: column;
gap: 0;
background: var(--card);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
padding: 10px 8px;
text-align: center;
cursor: pointer;
transition: all 0.15s;
margin-bottom: 16px;
}
.cat-card:hover { border-color: #a1a1aa; }
.cat-card.active { border-color: var(--fg); }
.cat-row {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 12px;
transition: background 0.15s;
}
.cat-row:not(:last-child) { border-bottom: 1px solid var(--border); }
.cat-row:hover { background: rgba(0,0,0,0.03); }
.cat-score {
font-size: 20px;
font-size: 16px;
font-weight: 700;
line-height: 1.2;
min-width: 32px;
text-align: right;
}
.cat-score.green { color: var(--green); }
.cat-score.amber { color: var(--amber); }
.cat-score.red { color: var(--red); }
.cat-label {
font-size: 10px;
color: var(--fg-muted);
margin-top: 2px;
font-size: 12px;
color: var(--fg);
flex: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.cat-bar {
flex: 1;
height: 4px;
background: var(--border);
border-radius: 2px;
overflow: hidden;
}
.cat-bar-fill {
height: 100%;
border-radius: 2px;
transition: width 0.3s;
}
.cat-issues {
font-size: 10px;
color: var(--fg-muted);
white-space: nowrap;
}

/* ---- Tooltips ---- */
.cic-tooltip {
position: fixed;
padding: 5px 10px;
background: #18181b;
color: #fafafa;
font-size: 11px;
font-weight: 400;
line-height: 1.4;
border-radius: 6px;
white-space: normal;
max-width: 220px;
width: max-content;
pointer-events: none;
opacity: 0;
transition: opacity 0.15s;
z-index: 1000;
}
.cic-tooltip.visible { opacity: 1; }
.cic-tooltip-arrow {
position: fixed;
width: 8px;
height: 8px;
background: #18181b;
transform: rotate(45deg);
pointer-events: none;
opacity: 0;
transition: opacity 0.15s;
z-index: 999;
}
.cic-tooltip-arrow.visible { opacity: 1; }

/* ---- Summary bar ---- */
.summary-bar {
display: flex;
Expand Down
9 changes: 6 additions & 3 deletions app/web/src/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ <h1 class="text-3xl font-bold tracking-tight">Analyze your Figma designs</h1>
</section>

<section class="bg-card border border-border rounded-lg shadow-sm p-6 mb-6 fade-in">
<div id="category-gauges" class="grid grid-cols-3 sm:grid-cols-6 gap-4"></div>
<div id="category-gauges" class="grid grid-cols-3 sm:grid-cols-5 gap-4"></div>
</section>

<section id="summary-section" class="bg-card border border-border rounded-lg shadow-sm p-4 mb-6 fade-in">
Expand Down Expand Up @@ -356,9 +356,11 @@ <h2 class="text-sm font-semibold flex items-center gap-2">

document.getElementById('category-gauges').innerHTML = CATEGORIES.map(function(cat) {
var cs = scores.byCategory[cat];
var desc = CATEGORY_DESCRIPTIONS[cat];
return '<a href="#cat-' + cat + '" class="flex flex-col items-center group relative cursor-pointer no-underline text-foreground hover:opacity-80 transition-opacity">' + renderGaugeSvg(cs.percentage, 100, 7) +
'<span class="text-xs font-medium mt-2.5 text-center leading-tight">' + CATEGORY_LABELS[cat] + '</span>' +
'<span class="text-[11px] text-muted-foreground">' + cs.issueCount + ' issues</span></a>';
'<span class="text-[11px] text-muted-foreground">' + cs.issueCount + ' issues</span>' +
'<div class="absolute bottom-full mb-2 left-1/2 -translate-x-1/2 hidden group-hover:block bg-zinc-900 text-white text-xs px-3 py-2 rounded-md max-w-[220px] w-max z-10 shadow-lg pointer-events-none">' + esc(desc) + '<div class="absolute top-full left-1/2 -translate-x-1/2 border-4 border-transparent border-t-zinc-900"></div></div></a>';
}).join('');

document.getElementById('summary-dots').innerHTML =
Expand Down Expand Up @@ -387,7 +389,8 @@ <h2 class="text-sm font-semibold flex items-center gap-2">
document.getElementById('footer-meta').textContent = new Date().toLocaleString() + ' \u00B7 ' + result.nodeCount + ' nodes \u00B7 Max depth ' + result.maxDepth;
}

function renderDot(cls, count, label) { return '<div class="flex items-center gap-2"><span class="w-2.5 h-2.5 rounded-full ' + cls + '"></span><span class="text-lg font-bold">' + count + '</span><span class="text-sm text-muted-foreground">' + label + '</span></div>'; }
var SEV_TIPS = { Blocking: 'Cannot implement correctly without fixing. Direct impact on screen reproduction.', Risk: 'Implementable now but will break or increase cost later.', 'Missing Info': 'Information is absent, forcing AI to guess.', Suggestion: 'Not immediately problematic, but improves systemization.' };
function renderDot(cls, count, label) { var tip = SEV_TIPS[label] || ''; return '<div class="flex items-center gap-2 relative group cursor-help"><span class="w-2.5 h-2.5 rounded-full ' + cls + '"></span><span class="text-lg font-bold">' + count + '</span><span class="text-sm text-muted-foreground">' + label + '</span><div class="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 hidden group-hover:block bg-zinc-900 text-white text-xs px-3 py-2 rounded-md max-w-[220px] w-max z-10 shadow-lg pointer-events-none">' + esc(tip) + '<div class="absolute top-full left-1/2 -translate-x-1/2 border-4 border-transparent border-t-zinc-900"></div></div></div>'; }
Comment on lines +392 to +393
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add a fallback tooltip title in renderDot for parity and resilience.

At Line 393, renderDot is hover-only and does not set a title, unlike the report renderer. Add the same fallback to keep behavior consistent.

🛠️ Suggested fix
-    function renderDot(cls, count, label) { var tip = SEV_TIPS[label] || ''; return '<div class="flex items-center gap-2 relative group cursor-help"><span class="w-2.5 h-2.5 rounded-full ' + cls + '"></span><span class="text-lg font-bold">' + count + '</span><span class="text-sm text-muted-foreground">' + label + '</span><div class="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 hidden group-hover:block bg-zinc-900 text-white text-xs px-3 py-2 rounded-md max-w-[220px] w-max z-10 shadow-lg pointer-events-none">' + esc(tip) + '<div class="absolute top-full left-1/2 -translate-x-1/2 border-4 border-transparent border-t-zinc-900"></div></div></div>'; }
+    function renderDot(cls, count, label) { var tip = SEV_TIPS[label] || ''; return '<div class="flex items-center gap-2 relative group cursor-help" title="' + esc(tip) + '"><span class="w-2.5 h-2.5 rounded-full ' + cls + '"></span><span class="text-lg font-bold">' + count + '</span><span class="text-sm text-muted-foreground">' + label + '</span><div class="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 hidden group-hover:block bg-zinc-900 text-white text-xs px-3 py-2 rounded-md max-w-[220px] w-max z-10 shadow-lg pointer-events-none">' + esc(tip) + '<div class="absolute top-full left-1/2 -translate-x-1/2 border-4 border-transparent border-t-zinc-900"></div></div></div>'; }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
var SEV_TIPS = { Blocking: 'Cannot implement correctly without fixing. Direct impact on screen reproduction.', Risk: 'Implementable now but will break or increase cost later.', 'Missing Info': 'Information is absent, forcing AI to guess.', Suggestion: 'Not immediately problematic, but improves systemization.' };
function renderDot(cls, count, label) { var tip = SEV_TIPS[label] || ''; return '<div class="flex items-center gap-2 relative group cursor-help"><span class="w-2.5 h-2.5 rounded-full ' + cls + '"></span><span class="text-lg font-bold">' + count + '</span><span class="text-sm text-muted-foreground">' + label + '</span><div class="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 hidden group-hover:block bg-zinc-900 text-white text-xs px-3 py-2 rounded-md max-w-[220px] w-max z-10 shadow-lg pointer-events-none">' + esc(tip) + '<div class="absolute top-full left-1/2 -translate-x-1/2 border-4 border-transparent border-t-zinc-900"></div></div></div>'; }
var SEV_TIPS = { Blocking: 'Cannot implement correctly without fixing. Direct impact on screen reproduction.', Risk: 'Implementable now but will break or increase cost later.', 'Missing Info': 'Information is absent, forcing AI to guess.', Suggestion: 'Not immediately problematic, but improves systemization.' };
function renderDot(cls, count, label) { var tip = SEV_TIPS[label] || ''; return '<div class="flex items-center gap-2 relative group cursor-help" title="' + esc(tip) + '"><span class="w-2.5 h-2.5 rounded-full ' + cls + '"></span><span class="text-lg font-bold">' + count + '</span><span class="text-sm text-muted-foreground">' + label + '</span><div class="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 hidden group-hover:block bg-zinc-900 text-white text-xs px-3 py-2 rounded-md max-w-[220px] w-max z-10 shadow-lg pointer-events-none">' + esc(tip) + '<div class="absolute top-full left-1/2 -translate-x-1/2 border-4 border-transparent border-t-zinc-900"></div></div></div>'; }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/web/src/index.html` around lines 392 - 393, The renderDot tooltip is
hover-only and lacks a fallback title like the report renderer; update the
renderDot function to set a title attribute on the outer container (or the
element that currently has class "cursor-help") using the resolved tip text (var
tip = SEV_TIPS[label] || '') and fall back to the label when tip is empty (e.g.,
title=esc(tip || label)), ensuring the existing hover tooltip remains unchanged.


function renderCat(cat, scores, issues, fk) {
var cs = scores.byCategory[cat], hp = issues.some(function(i) { return i.config.severity === 'blocking' || i.config.severity === 'risk'; });
Expand Down
13 changes: 11 additions & 2 deletions src/core/report-html/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ ${CATEGORIES.map(cat => {
${renderGaugeSvg(cs.percentage, 100, 7)}
<span class="text-xs font-medium mt-2.5 text-center leading-tight">${CATEGORY_LABELS[cat]}</span>
<span class="text-[11px] text-muted-foreground">${cs.issueCount} issues</span>
<div class="absolute bottom-full mb-2 left-1/2 -translate-x-1/2 hidden group-hover:block bg-zinc-900 text-white text-xs px-3 py-2 rounded-md whitespace-nowrap z-10 shadow-lg pointer-events-none">
<div class="absolute bottom-full mb-2 left-1/2 -translate-x-1/2 hidden group-hover:block bg-zinc-900 text-white text-xs px-3 py-2 rounded-md max-w-[220px] w-max z-10 shadow-lg pointer-events-none">
${esc(desc)}
<div class="absolute top-full left-1/2 -translate-x-1/2 border-4 border-transparent border-t-zinc-900"></div>
</div>
Expand Down Expand Up @@ -202,11 +202,20 @@ ${figmaToken ? ` <script>

// ---- Components ----

const SEVERITY_TOOLTIPS: Record<string, string> = {
Blocking: "Cannot implement correctly without fixing. Direct impact on screen reproduction.",
Risk: "Implementable now but will break or increase cost later.",
"Missing Info": "Information is absent, forcing AI to guess.",
Suggestion: "Not immediately problematic, but improves systemization.",
};
Comment on lines +205 to +210
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Move the severity tooltip text into shared constants.

These definitions are now duplicated here, in app/web/src/index.html (Lines 392-393), and in app/figma-plugin/src/ui.template.html (Lines 170-175), and the plugin copy already diverges (Missing vs Missing Info, different Risk wording). Please source this text from one shared constant next to SEVERITY_LABELS so every surface explains the severities the same way.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/report-html/index.ts` around lines 205 - 210, Create a single shared
constant for severity tooltips alongside SEVERITY_LABELS and replace the local
SEVERITY_TOOLTIPS with an import of that shared constant; ensure the shared
mapping uses the canonical keys and wording (e.g., "Missing Info" vs "Missing")
so all surfaces match, then update the other duplicated instances (the
HTML/template copies referenced) to consume the same shared constant or their
build-time equivalent so the texts are no longer duplicated and remain
consistent across SEVERITY_TOOLTIPS and SEVERITY_LABELS consumers.


function renderSummaryDot(dotClass: string, count: number, label: string): string {
return `<div class="flex items-center gap-2">
const tooltip = SEVERITY_TOOLTIPS[label] ?? "";
return `<div class="flex items-center gap-2 relative group cursor-help" title="${escapeHtml(tooltip)}">
<span class="w-2.5 h-2.5 rounded-full ${dotClass}"></span>
<span class="text-lg font-bold tracking-tight">${count}</span>
<span class="text-sm text-muted-foreground">${label}</span>
<div class="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 hidden group-hover:block bg-zinc-900 text-white text-xs px-3 py-2 rounded-md max-w-[220px] w-max z-10 shadow-lg pointer-events-none">${escapeHtml(tooltip)}<div class="absolute top-full left-1/2 -translate-x-1/2 border-4 border-transparent border-t-zinc-900"></div></div>
</div>`;
Comment on lines +213 to 219
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Make severity tooltip reachable by keyboard users.

At Line 214 and Line 218, the tooltip is hover-only on a non-focusable <div>, so keyboard users cannot reveal it.

♿ Suggested accessibility fix
-  return `<div class="flex items-center gap-2 relative group cursor-help" title="${escapeHtml(tooltip)}">
+  return `<div class="flex items-center gap-2 relative group cursor-help" title="${escapeHtml(tooltip)}" tabindex="0">
           <span class="w-2.5 h-2.5 rounded-full ${dotClass}"></span>
           <span class="text-lg font-bold tracking-tight">${count}</span>
           <span class="text-sm text-muted-foreground">${label}</span>
-          <div class="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 hidden group-hover:block bg-zinc-900 text-white text-xs px-3 py-2 rounded-md max-w-[220px] w-max z-10 shadow-lg pointer-events-none">${escapeHtml(tooltip)}<div class="absolute top-full left-1/2 -translate-x-1/2 border-4 border-transparent border-t-zinc-900"></div></div>
+          <div class="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 bg-zinc-900 text-white text-xs px-3 py-2 rounded-md max-w-[220px] w-max z-10 shadow-lg pointer-events-none opacity-0 group-hover:opacity-100 group-focus:opacity-100 transition-opacity">${escapeHtml(tooltip)}<div class="absolute top-full left-1/2 -translate-x-1/2 border-4 border-transparent border-t-zinc-900"></div></div>
         </div>`;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/report-html/index.ts` around lines 213 - 219, The tooltip is
currently hover-only on a non-focusable <div>, preventing keyboard users from
revealing it; update the HTML returned where `tooltip` is used (the template
string around `SEVERITY_TOOLTIPS[label]`) to make the container focusable (e.g.,
add tabindex="0" or use a <button>), add aria-describedby on the container
referencing a unique id for the tooltip, give the tooltip <div> a role="tooltip"
and that id, and change the visibility trigger to include focus (e.g., use
classes or attributes so the tooltip shows on group-hover OR group-focus /
:focus), ensuring you keep escaping via `escapeHtml(tooltip)`.

}

Expand Down
Loading