Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 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
54 changes: 46 additions & 8 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="' + (catTips[cat] || '') + '">' + (CATEGORY_LABELS[cat] || cat) + '</div>' +
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
'<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('');
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// Summary bar
var s = scores.summary;
var sevTips = { blocking: 'Cannot implement correctly without fixing.', risk: 'Implementable now but will break later.', missing: 'Information absent, AI must guess.', suggestion: 'Not 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="' + sevTips.blocking + '"><span class="dot dot-blocking"></span><span class="summary-count">' + s.blocking + '</span> Blocking</div>' +
'<div class="summary-item" data-tip="' + sevTips.risk + '"><span class="dot dot-risk"></span><span class="summary-count">' + s.risk + '</span> Risk</div>' +
'<div class="summary-item" data-tip="' + sevTips.missing + '"><span class="dot dot-missing"></span><span class="summary-count">' + s.missingInfo + '</span> Missing</div>' +
'<div class="summary-item" data-tip="' + sevTips.suggestion + '"><span class="dot dot-suggestion"></span><span class="summary-count">' + s.suggestion + '</span> Suggestion</div>' +
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
'<div class="summary-total">' + s.totalIssues + ' total</div>';

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

Severity tooltip wording diverges from other surfaces.

The sevTips object uses different keys and shorter text than the report-html and web versions:

Severity Plugin (here) Report/Web
Key missing "Missing Info"
Blocking "Cannot implement correctly without fixing." "Cannot implement correctly without fixing. Direct impact on screen reproduction."
Risk "Implementable now but will break later." "Implementable now but will break or increase cost later."

When consolidating to shared constants, ensure all surfaces use consistent wording.

🤖 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 170 - 176, Update the
sevTips object and the tooltip text used when building
document.getElementById('summary-bar').innerHTML so wording and keys match the
report-html/web copy: change sevTips.blocking to "Cannot implement correctly
without fixing. Direct impact on screen reproduction.", sevTips.risk to
"Implementable now but will break or increase cost later.", and ensure the
missing tooltip text matches "Missing Info" (or the shared constant used by
report-html/web) rather than "missing"; adjust any references in the summary-bar
construction that use sevTips and the s.* counts so they use those exact shared
strings (look for the sevTips object and the code building 'summary-bar').


// Filter tabs
Expand Down Expand Up @@ -343,6 +347,40 @@ <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('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 whitespace-nowrap 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>';
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
}).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 whitespace-nowrap 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 thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

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
11 changes: 10 additions & 1 deletion src/core/report-html/index.ts
Original file line number Diff line number Diff line change
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 px-3 py-1.5 bg-zinc-900 text-white text-xs rounded-md whitespace-nowrap opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none z-10">${escapeHtml(tooltip)}</div>
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
</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