diff --git a/app/figma-plugin/src/ui.template.html b/app/figma-plugin/src/ui.template.html
index 8fe745ab..838cbcb7 100644
--- a/app/figma-plugin/src/ui.template.html
+++ b/app/figma-plugin/src/ui.template.html
@@ -146,6 +146,7 @@
Ready to analyze
maxDepth: result.maxDepth,
});
el.className = 'visible';
+ CanICode.initReportInteractions(el);
document.getElementById('footer').style.display = '';
}
diff --git a/app/shared/styles.css b/app/shared/styles.css
index 1e8ea33f..ae6c5a7b 100644
--- a/app/shared/styles.css
+++ b/app/shared/styles.css
@@ -68,11 +68,11 @@ body {
================================================================ */
/* ---- Details/summary reset ---- */
-.rpt-cat > summary::-webkit-details-marker,
+.rpt-rule > summary::-webkit-details-marker,
.rpt-issue > summary::-webkit-details-marker { display: none; }
-.rpt-cat > summary::marker,
+.rpt-rule > summary::marker,
.rpt-issue > summary::marker { content: ""; }
-.rpt-cat > summary,
+.rpt-rule > summary,
.rpt-issue > summary { list-style: none; }
/* ---- Overall Score ---- */
@@ -115,10 +115,12 @@ body {
display: flex;
flex-direction: column;
align-items: center;
- position: relative;
cursor: pointer;
text-decoration: none;
color: var(--fg);
+ background: none;
+ border: none;
+ padding: 4px;
transition: opacity 0.15s;
}
.rpt-gauge-item:hover { opacity: 0.8; }
@@ -270,25 +272,80 @@ body {
}
.rpt-opps-link:hover { color: var(--fg); }
-/* ---- Categories ---- */
-.rpt-cats > * + * {
- margin-top: 12px;
+/* ---- Category Tabs (shadcn pill style) ---- */
+.rpt-tabs {
+ margin-bottom: 24px;
+ position: relative;
}
-.rpt-cat {
- overflow: hidden;
+@media (max-width: 600px) {
+ .rpt-tab-list::after {
+ content: "";
+ position: sticky;
+ right: 0;
+ width: 40px;
+ min-height: 100%;
+ background: linear-gradient(to right, transparent, var(--bg));
+ pointer-events: none;
+ flex-shrink: 0;
+ margin-left: -40px;
+ }
}
-.rpt-cat-header {
- padding: 14px 20px;
+.rpt-tab-list {
display: flex;
- align-items: center;
- gap: 12px;
+ gap: 4px;
+ padding: 4px;
+ background: var(--bg);
+ border: 1px solid var(--border);
+ border-radius: var(--radius);
+ overflow-x: auto;
+ white-space: nowrap;
+ -webkit-overflow-scrolling: touch;
+}
+.rpt-tab-list::-webkit-scrollbar { display: none; }
+.rpt-tab-list { scrollbar-width: none; }
+.rpt-tab {
+ padding: 6px 12px;
+ font-size: 13px;
+ font-weight: 500;
+ color: var(--fg-muted);
+ background: transparent;
+ border: none;
+ border-radius: 6px;
cursor: pointer;
- transition: background 0.15s;
- user-select: none;
+ transition: all 0.15s;
+ white-space: nowrap;
+ flex-shrink: 0;
+}
+.rpt-tab:hover { color: var(--fg); background: rgba(0,0,0,0.04); }
+.rpt-tab.active {
+ color: var(--fg);
+ background: var(--card);
+ box-shadow: 0 1px 2px rgba(0,0,0,0.06);
+}
+.rpt-tab-count {
+ font-size: 11px;
+ color: var(--fg-muted);
+ margin-left: 2px;
+}
+.rpt-tab.active .rpt-tab-count { color: var(--fg-muted); }
+.rpt-tab-panel { display: none; padding-top: 16px; }
+.rpt-tab-panel.active { display: block; }
+.rpt-cat-empty {
+ padding: 24px 20px;
+ font-size: 14px;
+ color: var(--green);
+ font-weight: 500;
+ text-align: center;
+}
+
+/* ---- Gauge item active highlight ---- */
+.rpt-gauge-item.active {
+ opacity: 1;
+ background: rgba(0,0,0,0.04);
+ border-radius: var(--radius);
}
-.rpt-cat-header:hover { background: rgba(0,0,0,0.02); }
-/* Score badge */
+/* ---- Score badge ---- */
.rpt-badge {
display: inline-flex;
align-items: center;
@@ -306,65 +363,76 @@ body {
.rpt-badge.score-amber { background: var(--amber-bg); color: #b45309; border-color: rgba(245,158,11,0.2); }
.rpt-badge.score-red { background: var(--red-bg); color: #b91c1c; border-color: rgba(239,68,68,0.2); }
-.rpt-cat-info {
- flex: 1;
- min-width: 0;
-}
-.rpt-cat-name {
- font-size: 14px;
- font-weight: 600;
+/* ---- Rule Section ---- */
+.rpt-tab-panel > .rpt-rule + .rpt-rule {
+ margin-top: 12px;
}
-.rpt-cat-desc {
- font-size: 12px;
- color: var(--fg-muted);
+.rpt-rule {
+ overflow: hidden;
}
-.rpt-cat-count {
- font-size: 12px;
- color: var(--fg-muted);
- white-space: nowrap;
+.rpt-rule-header {
+ padding: 14px 20px;
+ display: flex;
+ align-items: flex-start;
+ gap: 12px;
+ cursor: pointer;
+ user-select: none;
+ transition: background 0.15s;
}
-.rpt-cat-chevron {
+.rpt-rule-header:hover { background: rgba(0,0,0,0.02); }
+.rpt-rule-chevron {
width: 16px;
height: 16px;
color: var(--fg-muted);
transition: transform 0.2s;
flex-shrink: 0;
+ margin-top: 2px;
+ margin-left: auto;
}
-.rpt-cat[open] > .rpt-cat-header .rpt-cat-chevron {
+.rpt-rule[open] > .rpt-rule-header .rpt-rule-chevron {
transform: rotate(180deg);
}
-.rpt-cat-body {
- border-top: 1px solid var(--border);
-}
-.rpt-cat-empty {
- padding: 16px 20px;
+.rpt-rule-name {
font-size: 14px;
- color: var(--green);
- font-weight: 500;
-}
-
-/* ---- Severity Group ---- */
-.rpt-sev-group {
- padding: 12px 20px;
+ font-weight: 600;
+ display: block;
}
-.rpt-sev-header {
+.rpt-rule-meta {
+ font-size: 12px;
+ color: var(--fg-muted);
display: flex;
align-items: center;
- gap: 8px;
- margin-bottom: 8px;
+ gap: 6px;
+ margin-top: 2px;
}
-.rpt-sev-label {
- font-size: 11px;
- font-weight: 600;
- text-transform: uppercase;
- letter-spacing: 0.04em;
+.rpt-rule-title {
+ display: block;
+ flex: 1;
+ min-width: 0;
}
-.rpt-sev-count {
+.rpt-rule-info {
+ display: block;
+ padding: 8px 0 4px;
font-size: 12px;
color: var(--fg-muted);
- margin-left: auto;
+ line-height: 1.6;
+ margin-top: 8px;
+}
+.rpt-rule-info-line {
+ display: block;
+}
+.rpt-rule-info-line + .rpt-rule-info-line {
+ margin-top: 4px;
+}
+.rpt-rule-info strong {
+ color: var(--fg);
+ font-weight: 500;
}
-.rpt-sev-issues > * + * {
+.rpt-rule-issues {
+ padding: 8px 12px;
+ border-top: 1px solid var(--border);
+}
+.rpt-rule-issues > * + * {
margin-top: 4px;
}
@@ -379,16 +447,11 @@ body {
align-items: center;
gap: 10px;
padding: 8px 12px;
- font-size: 14px;
+ font-size: 13px;
cursor: pointer;
transition: background 0.15s;
}
.rpt-issue-header:hover { background: rgba(0,0,0,0.02); }
-.rpt-issue-name {
- font-weight: 500;
- white-space: nowrap;
- flex-shrink: 0;
-}
.rpt-issue-msg {
color: var(--fg-muted);
font-size: 12px;
@@ -415,10 +478,25 @@ body {
padding: 12px;
background: #fafafa;
border-top: 1px solid var(--border);
- font-size: 14px;
+ font-size: 13px;
}
.rpt-issue-body > * + * {
- margin-top: 8px;
+ margin-top: 6px;
+}
+.rpt-issue-suggestion {
+ font-weight: 500;
+ color: var(--fg);
+}
+.rpt-issue-guide {
+ font-size: 12px;
+ color: var(--fg-muted);
+ padding: 4px 8px;
+ background: var(--blue-bg);
+ border-radius: 4px;
+ display: inline-block;
+}
+.rpt-issue-guide::before {
+ content: "ℹ️ ";
}
.rpt-issue-path {
font-family: var(--font-mono);
@@ -426,17 +504,6 @@ body {
color: var(--fg-muted);
word-break: break-all;
}
-.rpt-issue-info {
- color: var(--fg-muted);
- line-height: 1.6;
-}
-.rpt-issue-info > * + * {
- margin-top: 4px;
-}
-.rpt-issue-info strong {
- color: var(--fg);
- font-weight: 500;
-}
.rpt-issue-actions {
display: flex;
align-items: center;
@@ -494,11 +561,12 @@ body {
.rpt-gauges { padding: 16px; }
.rpt-summary-inner { gap: 12px; }
.rpt-summary-total { border-left: none; padding-left: 0; }
+ .rpt-tab-list { gap: 8px; padding: 4px 12px; }
+ .rpt-tab { padding: 6px 14px; }
.rpt-opps-item { padding: 10px 16px; gap: 10px; }
.rpt-opps-bar-wrap { width: 80px; }
.rpt-opps-link { font-size: 11px; }
- .rpt-cat-header { padding: 10px 14px; gap: 8px; }
- .rpt-sev-group { padding: 10px 14px; }
+ .rpt-rule-header { padding: 10px 14px; gap: 8px; }
.rpt-issue-header { padding: 6px 10px; gap: 6px; font-size: 12px; }
.rpt-issue-body { padding: 10px; font-size: 12px; }
}
diff --git a/app/web/src/index.html b/app/web/src/index.html
index 494b730e..dd877dce 100644
--- a/app/web/src/index.html
+++ b/app/web/src/index.html
@@ -364,6 +364,7 @@ Analyze your Figma designs
figmaToken: getToken() || undefined,
});
el.classList.add('visible');
+ CanICode.initReportInteractions(el);
el.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
diff --git a/src/browser.ts b/src/browser.ts
index 2e88c06e..2fcec2cd 100644
--- a/src/browser.ts
+++ b/src/browser.ts
@@ -34,7 +34,7 @@ export {
} from "./core/ui-helpers.js";
// Report rendering (shared with web/plugin)
-export { renderReportBody } from "./core/report-html/render.js";
+export { renderReportBody, initReportInteractions } from "./core/report-html/render.js";
export type { ReportData } from "./core/report-html/render.js";
// Import rules to register them with the global registry
diff --git a/src/core/contracts/category.ts b/src/core/contracts/category.ts
index 2bdfcbb8..62121fcc 100644
--- a/src/core/contracts/category.ts
+++ b/src/core/contracts/category.ts
@@ -5,8 +5,8 @@ export const CategorySchema = z.enum([
"responsive-critical",
"code-quality",
"token-management",
- "interaction",
"minor",
+ "interaction",
]);
export type Category = z.infer;
@@ -18,6 +18,6 @@ export const CATEGORY_LABELS: Record = {
"responsive-critical": "Responsive Critical",
"code-quality": "Code Quality",
"token-management": "Token Management",
- "interaction": "Interaction",
"minor": "Minor",
+ "interaction": "Interaction",
};
diff --git a/src/core/contracts/rule.ts b/src/core/contracts/rule.ts
index 5cb211b2..786cb7e5 100644
--- a/src/core/contracts/rule.ts
+++ b/src/core/contracts/rule.ts
@@ -68,6 +68,8 @@ export interface RuleViolation {
nodeId: string;
nodePath: string;
message: string;
+ suggestion: string;
+ guide?: string;
}
/**
diff --git a/src/core/report-html/index.ts b/src/core/report-html/index.ts
index 5630cbaa..7bc22d25 100644
--- a/src/core/report-html/index.ts
+++ b/src/core/report-html/index.ts
@@ -5,14 +5,14 @@ import type { AnalysisFile } from "../contracts/figma-node.js";
import type { AnalysisResult } from "../engine/rule-engine.js";
import type { ScoreReport } from "../engine/scoring.js";
import { escapeHtml } from "../ui-helpers.js";
-import { renderReportBody } from "./render.js";
+import { renderReportBody, initReportInteractions } from "./render.js";
import type { ReportData } from "./render.js";
declare const __REPORT_CSS__: string;
const reportCss: string = __REPORT_CSS__;
export type { ReportData } from "./render.js";
-export { renderReportBody } from "./render.js";
+export { renderReportBody, initReportInteractions } from "./render.js";
export interface NodeScreenshot {
nodeId: string;
@@ -63,7 +63,6 @@ export function generateHtmlReport(