From 891b427ce857c8af54cca39201749d6467c80cff Mon Sep 17 00:00:00 2001 From: ssavutu Date: Wed, 5 Aug 2026 00:57:51 -0400 Subject: [PATCH 1/2] Use the vector Delta logo in the sidebar and as the favicon Replaces the raster logo.png wordmark with the SVG artwork, adds the triangle mark for the collapsed sidebar, and gives the app a favicon that is actually the product's own logo rather than The Triangle's blue "T". All three source files ship with the artwork sitting in a 377x347 canvas that the mark occupies a small band of, so each viewBox is tightened to the real bounding box. Left alone, object-contain would have scaled the wordmark to roughly a fifth of its intended size. Sizing changed with it. The new wordmark is 5.3:1 where the PNG was 2.8:1, so the old fixed w-30 h-30 square rendered it at half the visual height; it is now height-anchored. max-w-full replaces shrink-0 so it cannot spill past the 60px rail, which the PNG did. The collapsed mark is 40px rather than matching the wordmark's 28px because its two concentric rings merge into one muddy band below ~36px. Its rings are dropped entirely from the favicon, where 16px leaves no chance of resolving them. The favicon deliberately avoids CSS custom properties. A first pass used var(--mark) and rendering caught that librsvg drops it -- fill fell back to black and stroke: var() failed outright, silently deleting the crossbar. Browsers handle it, but favicons pass through enough non-browser pipelines that plain class rules are the safer choice. Its base color is brand blue because Safari ignores prefers-color-scheme in favicons; blue stays legible on both light and dark tab strips, and the media queries sharpen it to white or navy where they are honored. Verified the fallback path renders correctly. triangle.ico stays registered for browsers with no SVG favicon support. logo.png is left in place for now; it is no longer referenced. Co-Authored-By: Claude Opus 5 --- frontend/index.html | 3 ++- frontend/public/favicon.svg | 22 ++++++++++++++++++++++ frontend/src/assets/icon.svg | 1 + frontend/src/assets/logo.svg | 1 + frontend/src/components/Sidebar.tsx | 9 +++++++-- 5 files changed, 33 insertions(+), 3 deletions(-) create mode 100644 frontend/public/favicon.svg create mode 100644 frontend/src/assets/icon.svg create mode 100644 frontend/src/assets/logo.svg diff --git a/frontend/index.html b/frontend/index.html index 4777785..e095f09 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -2,7 +2,8 @@ - + + Delta CMS diff --git a/frontend/public/favicon.svg b/frontend/public/favicon.svg new file mode 100644 index 0000000..6a861b9 --- /dev/null +++ b/frontend/public/favicon.svg @@ -0,0 +1,22 @@ + + + + + + + + diff --git a/frontend/src/assets/icon.svg b/frontend/src/assets/icon.svg new file mode 100644 index 0000000..db57385 --- /dev/null +++ b/frontend/src/assets/icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/assets/logo.svg b/frontend/src/assets/logo.svg new file mode 100644 index 0000000..ab05121 --- /dev/null +++ b/frontend/src/assets/logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx index 997bc9c..fd94f24 100644 --- a/frontend/src/components/Sidebar.tsx +++ b/frontend/src/components/Sidebar.tsx @@ -23,7 +23,8 @@ import { } from "lucide-react" import { cn } from "@/lib/utils" import { Separator } from "@/components/ui/separator" -import logo from "../assets/logo.png" +import logo from "../assets/logo.svg" +import icon from "../assets/icon.svg" import { useCurrentUserRole } from "../hooks/useCurrentUserRole" import { useApiFetch } from "../hooks/useApiFetch" import { useSessionAuth } from "../auth/sessionAuthContext" @@ -158,7 +159,11 @@ export default function Sidebar() { collapsed && "justify-center px-0", )} > - Delta + {collapsed ? ( + Delta CMS + ) : ( + Delta CMS + )} {/* {!collapsed && ( Delta CMS )} */} From 4cdd686a272651da266d0f0117c4dcc2333f411f Mon Sep 17 00:00:00 2001 From: ssavutu Date: Wed, 5 Aug 2026 00:58:17 -0400 Subject: [PATCH 2/2] Rank Top Contributors by activity instead of by first appearance The card mapped over `users`, a Set built from the event feed, so it listed everyone who had ever done anything in whatever order their name first showed up. In practice that meant an unsorted list with someone sitting at the bottom on a single event -- it read as broken, because it was. Each row also recomputed its own count with a full events.filter(), so the card was O(users * events) for a number the same pass could have produced once. topContributors now counts into a Map in one pass, sorts by count descending with a name tiebreak, and caps at five. It is the same shape as the actionEntries memo directly above it, which was already doing this correctly. Capped rather than thresholded on purpose. A minimum-count filter would blank the card on a quiet install; a cap drops the single-event case while still showing everyone when there are only a few contributors. `users` stays deliberately unsorted, and that is the trap here: it is what pins a person to an avatar color, and the feed picks its colors from the same array by index. Sorting it in place would have recolored the feed and, worse, given the same person two different colors across the two panels. Both now go through one avatarColor helper. Also adds the empty state the Actions Breakdown card already had. Co-Authored-By: Claude Opus 5 --- frontend/src/pages/activityView.tsx | 41 ++++++++++++++++++++++------- 1 file changed, 32 insertions(+), 9 deletions(-) diff --git a/frontend/src/pages/activityView.tsx b/frontend/src/pages/activityView.tsx index 0416362..f1ad871 100644 --- a/frontend/src/pages/activityView.tsx +++ b/frontend/src/pages/activityView.tsx @@ -1,4 +1,4 @@ -import { useEffect, useMemo, useState } from "react" +import { useCallback, useEffect, useMemo, useState } from "react" import { Search, FileText, Tag, Settings, UserPlus, Users, RefreshCcw, Newspaper, ListTodo, Image, Upload, Trash2, MessageSquare } from "lucide-react" import { useApiFetch } from "../hooks/useApiFetch" @@ -69,6 +69,8 @@ const AVATAR_COLORS = [ "bg-rose-500", "bg-teal-500", "bg-indigo-500", "bg-amber-500", ] +const TOP_CONTRIBUTOR_LIMIT = 5 + function initials(name: string) { return name.split(" ").map((n) => n[0]).join("").slice(0, 2).toUpperCase() } @@ -101,8 +103,15 @@ export default function ActivityView() { .finally(() => setIsLoading(false)) }, [apiFetch]) + // First-appearance order. This is what pins a person to an avatar color, so + // it deliberately stays unsorted — reordering it would recolor the feed. const users = useMemo(() => [...new Set(events.map((event) => event.user || "System"))], [events]) + const avatarColor = useCallback( + (user: string) => AVATAR_COLORS[users.indexOf(user) % AVATAR_COLORS.length], + [users], + ) + const filtered = useMemo(() => events.filter((event) => { const meta = actionMeta(event.action) const query = search.toLowerCase() @@ -126,6 +135,20 @@ export default function ActivityView() { .sort((a, b) => b.count - a.count || a.meta.label.localeCompare(b.meta.label)) }, [events]) + // Busiest first and capped, so the card ranks people instead of just + // mirroring the order names happened to show up in the feed. + const topContributors = useMemo(() => { + const counts = new Map() + for (const event of events) { + const user = event.user || "System" + counts.set(user, (counts.get(user) ?? 0) + 1) + } + return [...counts.entries()] + .map(([user, count]) => ({ user, count })) + .sort((a, b) => b.count - a.count || a.user.localeCompare(b.user)) + .slice(0, TOP_CONTRIBUTOR_LIMIT) + }, [events]) + return (
@@ -164,10 +187,9 @@ export default function ActivityView() { const meta = actionMeta(event.action) const Icon = meta.icon const user = event.user || "System" - const userIdx = users.indexOf(user) return (
-
+
{initials(user)}
@@ -192,18 +214,19 @@ export default function ActivityView() {

Top Contributors

- {users.map((user, i) => { - const count = events.filter((event) => (event.user || "System") === user).length - return ( + {topContributors.length === 0 ? ( + No contributors yet. + ) : ( + topContributors.map(({ user, count }) => (
-
+
{initials(user)}
{user.split(" ")[0]} {count}
- ) - })} + )) + )}