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
36 changes: 36 additions & 0 deletions app/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,40 @@ function CategorySection({ title, items, defaultOpen, quarantineDir, onQuarantin
);
}

/**
* Compact summary showing top categories ranked by reclaimable bytes.
* Gives users an immediate overview of where space is consumed
* before they dive into individual expandable sections.
*/
function TopCategoriesSummary({ items, cap = 5 }) {
if (!items || items.length === 0) return null;

const categories = byCategory(items)
.map(([cat, catItems]) => ({
key: cat,
label: CATEGORY_LABEL[cat] ?? cat,
total: catItems.reduce((s, f) => s + f.reclaimable, 0),
}))
.filter((c) => c.total > 0)
.slice(0, cap);

if (categories.length === 0) return null;

return (
<div className="top-categories-summary" aria-label="Top categories">
<h3 className="top-categories-title">Top categories</h3>
<ul className="top-categories-list">
{categories.map((c) => (
<li key={c.key} className="top-category-row">
<span className="top-category-label">{c.label}</span>
<span className="top-category-size">{humanBytes(c.total)}</span>
</li>
))}
</ul>
</div>
);
}

function DuplicatesSection({ sets }) {
const [isOpen, setIsOpen] = useState(true);
if (sets.length === 0) return null;
Expand Down Expand Up @@ -673,6 +707,8 @@ export default function App() {
onRestored={handleRestored}
/>

<TopCategoriesSummary items={visibleFindings} />

<DuplicatesSection sets={duplicateSets} />
<CategorySection
title="Safe to remove"
Expand Down
18 changes: 18 additions & 0 deletions app/src/App.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -125,3 +125,21 @@ test("the region is empty while idle and announces cancelling", () => {
"pressing Cancel should announce that the scan is stopping"
);
});

test("shows a compact top-categories summary above the detailed results", () => {
assert.match(jsx, /function TopCategoriesSummary/);
assert.match(jsx, /className="top-categories-summary"/);
assert.match(jsx, /<h3 className="top-categories-title">Top categories<\/h3>/);
assert.match(jsx, /className="top-categories-list"/);
assert.match(jsx, /className="top-category-row"/);
assert.match(jsx, /<TopCategoriesSummary items=\{visibleFindings\} \/>/);

// Verifies TopCategoriesSummary is mounted above DuplicatesSection and CategorySection
const summaryIdx = jsx.indexOf("<TopCategoriesSummary");
const duplicatesIdx = jsx.indexOf("<DuplicatesSection");
const categorySectionIdx = jsx.indexOf("<CategorySection");
assert.ok(summaryIdx > 0, "TopCategoriesSummary must be rendered in App");
assert.ok(summaryIdx < duplicatesIdx, "TopCategoriesSummary must render above DuplicatesSection");
assert.ok(summaryIdx < categorySectionIdx, "TopCategoriesSummary must render above CategorySection");
});

37 changes: 37 additions & 0 deletions app/src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,43 @@ button { padding: 0.6rem 1.4rem; font-size: 1rem; cursor: pointer; }
.summary { line-height: 1.5; }
.scanned-folder { font-family: ui-monospace, monospace; font-size: 0.85rem; opacity: 0.7; }

/* Top categories compact summary */
.top-categories-summary {
margin: 1rem 0 0.5rem;
padding: 0.75rem 1rem;
background: rgba(128, 128, 128, 0.06);
border: 1px solid rgba(128, 128, 128, 0.2);
border-radius: 8px;
}
.top-categories-title {
margin: 0 0 0.5rem;
font-size: 0.9rem;
font-weight: 600;
opacity: 0.85;
}
.top-categories-list {
list-style: none;
padding: 0;
margin: 0;
display: grid;
gap: 0.35rem;
}
.top-category-row {
display: flex;
justify-content: space-between;
align-items: baseline;
font-size: 0.85rem;
}
.top-category-label {
opacity: 0.85;
}
.top-category-size {
font-family: ui-monospace, monospace;
font-size: 0.85rem;
font-weight: 500;
opacity: 0.9;
}

/* Collapsible verdict/category groups */
.verdict-group { margin-top: 1.25rem; }
.group-header {
Expand Down
8 changes: 8 additions & 0 deletions app/src/styles.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -101,3 +101,11 @@ test("category badges have quiet styling", () => {
assert.match(css, /\.category-badge\s*\{[^}]*font-size\s*:/s);
});

test("top categories summary has compact card styling", () => {
assert.match(css, /\.top-categories-summary\s*\{[^}]*border-radius\s*:/s);
assert.match(css, /\.top-categories-title\s*\{[^}]*font-weight\s*:/s);
assert.match(css, /\.top-category-row\s*\{[^}]*display\s*:\s*flex\b/s);
assert.match(css, /\.top-category-size\s*\{[^}]*font-family\s*:\s*ui-monospace/s);
});


2 changes: 2 additions & 0 deletions changelog.d/158-top-categories-summary.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
- Show a compact top-categories summary card above detailed scan results
ranking space by reclaimable bytes for both preview and final reports.
Loading