diff --git a/app/src/App.jsx b/app/src/App.jsx
index de593f5..755a969 100644
--- a/app/src/App.jsx
+++ b/app/src/App.jsx
@@ -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 (
+
+
Top categories
+
+ {categories.map((c) => (
+ -
+ {c.label}
+ {humanBytes(c.total)}
+
+ ))}
+
+
+ );
+}
+
function DuplicatesSection({ sets }) {
const [isOpen, setIsOpen] = useState(true);
if (sets.length === 0) return null;
@@ -673,6 +707,8 @@ export default function App() {
onRestored={handleRestored}
/>
+
+
{
"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, /Top categories<\/h3>/);
+ assert.match(jsx, /className="top-categories-list"/);
+ assert.match(jsx, /className="top-category-row"/);
+ assert.match(jsx, //);
+
+ // Verifies TopCategoriesSummary is mounted above DuplicatesSection and CategorySection
+ const summaryIdx = jsx.indexOf(" 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");
+});
+
diff --git a/app/src/styles.css b/app/src/styles.css
index 118ec7b..8f9aacc 100644
--- a/app/src/styles.css
+++ b/app/src/styles.css
@@ -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 {
diff --git a/app/src/styles.test.mjs b/app/src/styles.test.mjs
index ab8c2a0..8bd7bc4 100644
--- a/app/src/styles.test.mjs
+++ b/app/src/styles.test.mjs
@@ -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);
+});
+
+
diff --git a/changelog.d/158-top-categories-summary.added.md b/changelog.d/158-top-categories-summary.added.md
new file mode 100644
index 0000000..22e2126
--- /dev/null
+++ b/changelog.d/158-top-categories-summary.added.md
@@ -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.