From fa05bff33cd58d587d03a728703fb328bb4303dc Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 31 Mar 2026 09:48:27 +0000 Subject: [PATCH 1/3] feat(report): group categories into Pixel Accuracy / Token Efficiency cards (#215) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace 6 individual category gauges with 2 group cards showing bar charts per category. Display-only change — no scoring formula modification. Each group shows average score, description, and clickable category bars that navigate to the corresponding tab. https://claude.ai/code/session_01H2EPyD3PhqkQFrk5BB4M4g --- app/shared/styles.css | 99 ++++++++++++++++++++++------- src/core/report-html/render.test.ts | 48 +++++++++++++- src/core/report-html/render.ts | 52 +++++++++++---- src/core/ui-constants.ts | 28 ++++++++ 4 files changed, 188 insertions(+), 39 deletions(-) diff --git a/app/shared/styles.css b/app/shared/styles.css index e7ffa59a..3a9316c3 100644 --- a/app/shared/styles.css +++ b/app/shared/styles.css @@ -101,39 +101,93 @@ body { margin-top: 4px; } -/* ---- Category Gauges ---- */ -.rpt-gauges { - padding: 24px; - margin-bottom: 24px; -} -.rpt-gauges-grid { +/* ---- Category Groups (#215) ---- */ +.rpt-groups { display: grid; - grid-template-columns: repeat(6, 1fr); + grid-template-columns: repeat(2, 1fr); gap: 16px; + margin-bottom: 24px; +} +.rpt-group { + padding: 0; + overflow: hidden; +} +.rpt-group-header { + padding: 16px 20px 12px; + border-bottom: 1px solid var(--border); } -.rpt-gauge-item { +.rpt-group-title-row { display: flex; - flex-direction: column; align-items: center; - cursor: pointer; - text-decoration: none; + gap: 10px; +} +.rpt-group-pct { + font-size: 20px; + font-weight: 700; + letter-spacing: -0.02em; +} +.rpt-group-pct.score-green { color: #15803d; } +.rpt-group-pct.score-amber { color: #b45309; } +.rpt-group-pct.score-red { color: #b91c1c; } +.rpt-group-title { + font-size: 14px; + font-weight: 600; color: var(--fg); +} +.rpt-group-desc { + font-size: 12px; + color: var(--fg-muted); + margin-top: 4px; + line-height: 1.5; +} +.rpt-group-bars { + padding: 8px 0; +} +.rpt-group-bar-item { + display: grid; + grid-template-columns: 1fr 1fr 40px 28px; + align-items: center; + gap: 10px; + padding: 6px 20px; + width: 100%; background: none; border: none; - padding: 4px; - transition: opacity 0.15s; + cursor: pointer; + transition: background 0.15s; + color: var(--fg); + font-family: var(--font-sans); } -.rpt-gauge-item:hover { opacity: 0.8; } -.rpt-gauge-label { - font-size: 12px; +.rpt-group-bar-item:hover { background: rgba(0,0,0,0.03); } +.rpt-group-bar-label { + font-size: 13px; font-weight: 500; - margin-top: 10px; - text-align: center; - line-height: 1.3; + text-align: left; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } -.rpt-gauge-count { +.rpt-group-bar-track { + height: 6px; + background: var(--border); + border-radius: 3px; + overflow: hidden; +} +.rpt-group-bar-fill { + height: 100%; + border-radius: 3px; + transition: width 0.4s ease; +} +.rpt-group-bar-pct { + font-size: 12px; + font-weight: 600; + text-align: right; + font-variant-numeric: tabular-nums; +} +.rpt-group-bar-count { font-size: 11px; color: var(--fg-muted); + text-align: right; + font-variant-numeric: tabular-nums; } @@ -547,11 +601,10 @@ body { 4. Responsive — narrow viewport (Figma plugin ~420px) ================================================================ */ @media (max-width: 600px) { - .rpt-gauges-grid { - grid-template-columns: repeat(3, 1fr); + .rpt-groups { + grid-template-columns: 1fr; } .rpt-overall { padding: 24px 0 16px; } - .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; } diff --git a/src/core/report-html/render.test.ts b/src/core/report-html/render.test.ts index 92cba625..23375d50 100644 --- a/src/core/report-html/render.test.ts +++ b/src/core/report-html/render.test.ts @@ -7,6 +7,7 @@ import type { ScoreReport, CategoryScoreResult } from "../engine/scoring.js"; import { renderReportBody, renderSummaryDot, + renderCategoryGroup, renderOpportunities, renderRuleSection, renderIssueRow, @@ -128,11 +129,15 @@ describe("renderReportBody", () => { expect(html).toContain(">80<"); }); - it("renders category gauge buttons", () => { + it("renders category group cards with bar charts (#215)", () => { const html = renderReportBody(makeReportData()); - expect(html).toContain('class="rpt-gauges-grid"'); + expect(html).toContain('class="rpt-groups"'); + expect(html).toContain('data-group="pixel-accuracy"'); + expect(html).toContain('data-group="token-efficiency"'); + expect(html).toContain("Pixel Accuracy"); + expect(html).toContain("Token Efficiency"); + expect(html).toContain('class="rpt-group-bar-item"'); expect(html).toContain('data-tab="pixel-critical"'); - expect(html).toContain('class="rpt-gauge-label"'); }); it("renders issue summary", () => { @@ -207,6 +212,43 @@ describe("renderSummaryDot", () => { }); }); +// ---- renderCategoryGroup ---- + +describe("renderCategoryGroup", () => { + it("renders group card with average score and category bars", () => { + const scores = makeScores(); + const group = { + id: "pixel-accuracy", + label: "Pixel Accuracy", + description: "How much AI can implement without guessing.", + categories: ["pixel-critical", "responsive-critical"] as Category[], + }; + const html = renderCategoryGroup(group, scores); + expect(html).toContain('data-group="pixel-accuracy"'); + expect(html).toContain("Pixel Accuracy"); + expect(html).toContain("How much AI can implement without guessing."); + expect(html).toContain("80%"); // average of both categories at 80% + expect(html).toContain('data-tab="pixel-critical"'); + expect(html).toContain('data-tab="responsive-critical"'); + expect(html).toContain('class="rpt-group-bar-track"'); + }); + + it("renders group description for context", () => { + const scores = makeScores(); + const group = { + id: "token-efficiency", + label: "Token Efficiency", + description: "How efficiently AI can work with the design.", + categories: ["code-quality", "token-management", "semantic", "interaction"] as Category[], + }; + const html = renderCategoryGroup(group, scores); + expect(html).toContain("Token Efficiency"); + expect(html).toContain("How efficiently AI can work with the design."); + // 4 bar items + expect(html.match(/rpt-group-bar-item/g)?.length).toBe(4); + }); +}); + // ---- renderOpportunities ---- describe("renderOpportunities", () => { diff --git a/src/core/report-html/render.ts b/src/core/report-html/render.ts index 8fec700d..b9a13a08 100644 --- a/src/core/report-html/render.ts +++ b/src/core/report-html/render.ts @@ -18,12 +18,16 @@ import { buildFigmaDeepLink } from "../adapters/figma-url-parser.js"; import { CATEGORIES, CATEGORY_LABELS, + CATEGORY_GROUPS, } from "../ui-constants.js"; +import type { CategoryGroup } from "../ui-constants.js"; import { escapeHtml, severityDot, severityBadge, renderGaugeSvg, + gaugeColor, + scoreClass, } from "../ui-helpers.js"; // ---- Data interface ---- @@ -73,19 +77,10 @@ export function renderReportBody(data: ReportData): string {

Overall Score

- -
-
-${CATEGORIES.map((cat) => { - const cs = scores.byCategory[cat]; - return ` `; - }).join("\n")} -
-
+ +
+${CATEGORY_GROUPS.map(group => renderCategoryGroup(group, scores)).join("\n")} +
@@ -141,6 +136,37 @@ export function renderSummaryDot(sevClass: string, count: number, label: string) `; } +export function renderCategoryGroup( + group: CategoryGroup, + scores: ScoreReport, +): string { + const catScores = group.categories.map(cat => scores.byCategory[cat]); + const avgPct = Math.round( + catScores.reduce((sum, cs) => sum + cs.percentage, 0) / catScores.length, + ); + const colorClass = scoreClass(avgPct); + + return `
+
+
+ ${avgPct}% +

${esc(group.label)}

+
+

${esc(group.description)}

+
+
+${catScores.map(cs => ` `).join("\n")} +
+
`; +} + export function renderOpportunities(ruleGroups: RuleGroup[]): string { const maxAbs = ruleGroups.reduce((m, rg) => Math.max(m, Math.abs(rg.totalScore)), 1); return ` diff --git a/src/core/ui-constants.ts b/src/core/ui-constants.ts index 27d50141..303df5fe 100644 --- a/src/core/ui-constants.ts +++ b/src/core/ui-constants.ts @@ -11,6 +11,34 @@ export { SEVERITY_LABELS } from "./contracts/severity.js"; export const GAUGE_R = 54; export const GAUGE_C = Math.round(2 * Math.PI * GAUGE_R); // ~339 +/** + * Category groups for report display (#215). + * Groups 6 categories into two dimensions so users can see + * "pixel accuracy is fine, token efficiency is dragging the grade." + * Display-only — does not affect score calculation. + */ +export interface CategoryGroup { + id: string; + label: string; + description: string; + categories: Category[]; +} + +export const CATEGORY_GROUPS: CategoryGroup[] = [ + { + id: "pixel-accuracy", + label: "Pixel Accuracy", + description: "How much AI can implement without guessing — layout and size information is clear enough for deterministic results.", + categories: ["pixel-critical", "responsive-critical"], + }, + { + id: "token-efficiency", + label: "Token Efficiency", + description: "How efficiently AI can work with the design — tokens and components are organized to minimize waste.", + categories: ["code-quality", "token-management", "semantic", "interaction"], + }, +]; + export const CATEGORY_DESCRIPTIONS: Record = { "pixel-critical": "Auto Layout, absolute positioning, group usage — layout issues that directly affect pixel accuracy", From 396219b2ee100267a8b00ecfda3ec6633bb06d9d Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 31 Mar 2026 09:52:49 +0000 Subject: [PATCH 2/3] fix(report): keep 2-column groups on mobile, hide bar graphs On mobile (<600px), hide category bar charts so both group cards fit on screen at a glance. Keep 2-column grid layout. https://claude.ai/code/session_01H2EPyD3PhqkQFrk5BB4M4g --- app/shared/styles.css | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app/shared/styles.css b/app/shared/styles.css index 3a9316c3..74ffb4bf 100644 --- a/app/shared/styles.css +++ b/app/shared/styles.css @@ -601,9 +601,8 @@ body { 4. Responsive — narrow viewport (Figma plugin ~420px) ================================================================ */ @media (max-width: 600px) { - .rpt-groups { - grid-template-columns: 1fr; - } + .rpt-group-bars { display: none; } + .rpt-group-header { border-bottom: none; } .rpt-overall { padding: 24px 0 16px; } .rpt-summary-inner { gap: 12px; } .rpt-summary-total { border-left: none; padding-left: 0; } From 4298e14828f80cf25fbe7f29dad4aeca87b76fa3 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 31 Mar 2026 10:07:41 +0000 Subject: [PATCH 3/3] feat(report): add compact category view for mobile groups Show inline category name + percentage below group description on mobile (<600px) when bar graphs are hidden. Desktop unchanged. https://claude.ai/code/session_01H2EPyD3PhqkQFrk5BB4M4g --- app/shared/styles.css | 19 +++++++++++++++++++ src/core/report-html/render.ts | 3 +++ 2 files changed, 22 insertions(+) diff --git a/app/shared/styles.css b/app/shared/styles.css index 74ffb4bf..3e050ee2 100644 --- a/app/shared/styles.css +++ b/app/shared/styles.css @@ -189,6 +189,24 @@ body { text-align: right; font-variant-numeric: tabular-nums; } +/* Compact inline view — hidden on desktop, shown on mobile */ +.rpt-group-compact { + display: none; + flex-wrap: wrap; + gap: 4px 12px; + margin-top: 8px; +} +.rpt-group-compact-item { + font-size: 12px; + cursor: pointer; +} +.rpt-group-compact-label { + color: var(--fg-muted); +} +.rpt-group-compact-pct { + font-weight: 600; + font-variant-numeric: tabular-nums; +} /* ---- Issue Summary ---- */ @@ -602,6 +620,7 @@ body { ================================================================ */ @media (max-width: 600px) { .rpt-group-bars { display: none; } + .rpt-group-compact { display: flex; } .rpt-group-header { border-bottom: none; } .rpt-overall { padding: 24px 0 16px; } .rpt-summary-inner { gap: 12px; } diff --git a/src/core/report-html/render.ts b/src/core/report-html/render.ts index b9a13a08..de57b47c 100644 --- a/src/core/report-html/render.ts +++ b/src/core/report-html/render.ts @@ -153,6 +153,9 @@ export function renderCategoryGroup(

${esc(group.label)}

${esc(group.description)}

+
+${catScores.map(cs => ` ${CATEGORY_LABELS[cs.category]} ${cs.percentage}%`).join("\n")} +
${catScores.map(cs => `