From 352e31af00185fe65b53d2e011ea058183fe6ef5 Mon Sep 17 00:00:00 2001 From: teddyvj Date: Sun, 13 Sep 2026 20:28:46 +0500 Subject: [PATCH] feat(app): show a compact top-categories summary after scans (#158) - Add TopCategoriesSummary component ranking categories by reclaimable bytes - Render summary above detailed results for both live preview and final reports - Compact UI displaying category label and formatted reclaimable bytes - Add styling for summary card, list, and monospace size values - Add unit and styling tests in App.test.mjs and styles.test.mjs - Add changelog fragment in changelog.d/158-top-categories-summary.added.md --- app/src/App.jsx | 36 ++++++++++++++++++ app/src/App.test.mjs | 18 +++++++++ app/src/styles.css | 37 +++++++++++++++++++ app/src/styles.test.mjs | 8 ++++ .../158-top-categories-summary.added.md | 2 + 5 files changed, 101 insertions(+) create mode 100644 changelog.d/158-top-categories-summary.added.md 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

+ +
+ ); +} + 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.