Skip to content
Closed
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
119 changes: 95 additions & 24 deletions app/shared/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -101,39 +101,111 @@ 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-group-bar-track {
height: 6px;
background: var(--border);
border-radius: 3px;
overflow: hidden;
}
.rpt-gauge-count {
.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;
}
/* 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;
}


Expand Down Expand Up @@ -547,11 +619,10 @@ body {
4. Responsive — narrow viewport (Figma plugin ~420px)
================================================================ */
@media (max-width: 600px) {
.rpt-gauges-grid {
grid-template-columns: repeat(3, 1fr);
}
.rpt-group-bars { display: none; }
.rpt-group-compact { display: flex; }
.rpt-group-header { border-bottom: none; }
.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; }
Expand Down
48 changes: 45 additions & 3 deletions src/core/report-html/render.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type { ScoreReport, CategoryScoreResult } from "../engine/scoring.js";
import {
renderReportBody,
renderSummaryDot,
renderCategoryGroup,
renderOpportunities,
renderRuleSection,
renderIssueRow,
Expand Down Expand Up @@ -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", () => {
Expand Down Expand Up @@ -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", () => {
Expand Down
55 changes: 42 additions & 13 deletions src/core/report-html/render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 ----
Expand Down Expand Up @@ -73,19 +77,10 @@ export function renderReportBody(data: ReportData): string {
<p class="rpt-score-label">Overall Score</p>
</section>

<!-- Category Gauges -->
<section class="card rpt-gauges">
<div class="rpt-gauges-grid">
${CATEGORIES.map((cat) => {
const cs = scores.byCategory[cat];
return ` <button type="button" class="rpt-gauge-item" data-tab="${cat}">
${renderGaugeSvg(cs.percentage, 100, 7)}
<span class="rpt-gauge-label">${CATEGORY_LABELS[cat]}</span>
<span class="rpt-gauge-count">${cs.issueCount} issues</span>
</button>`;
}).join("\n")}
</div>
</section>
<!-- Category Groups (#215) -->
<div class="rpt-groups">
${CATEGORY_GROUPS.map(group => renderCategoryGroup(group, scores)).join("\n")}
</div>

<!-- Issue Summary -->
<section class="card rpt-summary">
Expand Down Expand Up @@ -141,6 +136,40 @@ export function renderSummaryDot(sevClass: string, count: number, label: string)
</div>`;
}

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 ` <section class="card rpt-group" data-group="${group.id}">
<div class="rpt-group-header">
<div class="rpt-group-title-row">
<span class="rpt-group-pct score-${colorClass}">${avgPct}%</span>
<h2 class="rpt-group-title">${esc(group.label)}</h2>
</div>
<p class="rpt-group-desc">${esc(group.description)}</p>
<div class="rpt-group-compact">
${catScores.map(cs => ` <span class="rpt-group-compact-item" data-tab="${cs.category}"><span class="rpt-group-compact-label">${CATEGORY_LABELS[cs.category]}</span> <span class="rpt-group-compact-pct" style="color:${gaugeColor(cs.percentage)}">${cs.percentage}%</span></span>`).join("\n")}
</div>
</div>
<div class="rpt-group-bars">
${catScores.map(cs => ` <button type="button" class="rpt-group-bar-item" data-tab="${cs.category}">
<span class="rpt-group-bar-label">${CATEGORY_LABELS[cs.category]}</span>
<div class="rpt-group-bar-track">
<div class="rpt-group-bar-fill" style="width:${cs.percentage}%;background:${gaugeColor(cs.percentage)}"></div>
</div>
<span class="rpt-group-bar-pct">${cs.percentage}%</span>
<span class="rpt-group-bar-count">${cs.issueCount}</span>
</button>`).join("\n")}
</div>
</section>`;
}

export function renderOpportunities(ruleGroups: RuleGroup[]): string {
const maxAbs = ruleGroups.reduce((m, rg) => Math.max(m, Math.abs(rg.totalScore)), 1);
return `
Expand Down
28 changes: 28 additions & 0 deletions src/core/ui-constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Category, string> = {
"pixel-critical":
"Auto Layout, absolute positioning, group usage — layout issues that directly affect pixel accuracy",
Expand Down
Loading