From 0af17bd98b55e9508c645a0c84a6b9f1e35f70b0 Mon Sep 17 00:00:00 2001
From: miraclesonly
Date: Tue, 25 Aug 2026 09:04:58 +0100
Subject: [PATCH 1/4] fix(dashboard-shell): highlight the parent nav item on
nested routes
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Active-nav-item detection used exact pathname equality
(pathname === item.href). A maintainer navigating from the pipeline
board into a specific bounty (/issues/abc123, reached via
PipelineBoard.tsx's own links) made pathname !== "/issues", so
"Bounty pipeline" lost its highlighted state exactly while the user
was drilled into a page that's conceptually still part of that
section — the sidebar appeared to have nothing selected, a common and
expected navigation state this exact-match check didn't account for.
Added a prefix check for non-overview items (anything not under
/dashboard/*) so a nested route like /issues/abc123 keeps its parent
section highlighted, while /dashboard/* overview links keep exact
matching so they don't stay highlighted while browsing unrelated
sections (closes #220).
---
src/components/dashboard/DashboardShell.tsx | 11 ++++++++++-
1 file changed, 10 insertions(+), 1 deletion(-)
diff --git a/src/components/dashboard/DashboardShell.tsx b/src/components/dashboard/DashboardShell.tsx
index 1f07793..5760cce 100644
--- a/src/components/dashboard/DashboardShell.tsx
+++ b/src/components/dashboard/DashboardShell.tsx
@@ -71,7 +71,16 @@ export function DashboardShell({
{items.map((item) => {
- const active = pathname === item.href;
+ // Overview links (/dashboard/*) use exact matching so they
+ // don't stay highlighted while browsing unrelated sections.
+ // Every other item (e.g. /issues, /milestones) also matches
+ // its nested routes (/issues/abc123), so drilling into a
+ // specific bounty/milestone keeps its parent section
+ // highlighted instead of the sidebar appearing to have
+ // nothing selected (#220).
+ const active =
+ pathname === item.href ||
+ (!item.href.startsWith("/dashboard/") && pathname.startsWith(`${item.href}/`));
return (
Date: Tue, 25 Aug 2026 09:05:10 +0100
Subject: [PATCH 2/4] fix(footer): stop the copyright year from freezing on
statically-optimized builds
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Footer.tsx computed new Date().getFullYear() directly in a Server
Component with no dynamic API usage of its own. Next.js statically
optimizes any route it can prove has no per-request dynamic data, so
that call was evaluated once, at build time, not per-request, on any
route Next was able to optimize this way — nothing in Footer.tsx
declared force-dynamic or otherwise opted into per-request evaluation,
so the correctness of the displayed year was incidentally dependent on
unrelated routes' fetch behavior. On a statically-optimized route, the
year silently froze at whatever `next build` was run and stayed wrong
for the rest of that build's lifetime, with nothing surfacing the
staleness.
Extracted the year into a new CopyrightYear Client Component: the
initial render still uses whatever Date.now() was available at
build/SSR time, but a useEffect always runs client-side on every
hydration and re-reads the browser's actual current date, correcting
any stale build-time year the moment a visitor's page hydrates rather
than waiting for the next deploy (closes #221).
---
src/components/layout/CopyrightYear.tsx | 32 +++++++++++++++++++++++++
src/components/layout/Footer.tsx | 5 +++-
2 files changed, 36 insertions(+), 1 deletion(-)
create mode 100644 src/components/layout/CopyrightYear.tsx
diff --git a/src/components/layout/CopyrightYear.tsx b/src/components/layout/CopyrightYear.tsx
new file mode 100644
index 0000000..32106d5
--- /dev/null
+++ b/src/components/layout/CopyrightYear.tsx
@@ -0,0 +1,32 @@
+"use client";
+
+import { useEffect, useState } from "react";
+
+/**
+ * Footer is a Server Component with no dynamic API usage of its own, so
+ * Next.js can statically optimize routes that render it — new Date() called
+ * server-side is then evaluated once, at build time, not per-request. On a
+ * statically-optimized route this silently froze the displayed year at
+ * whatever `next build` was run, with nothing correcting it for the rest of
+ * that build's lifetime (#221).
+ *
+ * Isolating just the year into this small Client Component lets it
+ * self-correct on every page load: the initial render (SSR/build-time) uses
+ * whatever Date.now() was available then, but the effect below always runs
+ * client-side, on every hydration, and re-reads the browser's actual
+ * current date — so a stale build-time year gets fixed the moment any
+ * visitor's browser hydrates the page, rather than staying wrong until the
+ * next deploy.
+ */
+export function CopyrightYear() {
+ const [year, setYear] = useState(() => new Date().getFullYear());
+
+ useEffect(() => {
+ // Corrects a stale build-time year on mount — the same pattern used
+ // elsewhere in this app for reconciling client-only state after hydration.
+ // eslint-disable-next-line react-hooks/set-state-in-effect
+ setYear(new Date().getFullYear());
+ }, []);
+
+ return <>{year}>;
+}
diff --git a/src/components/layout/Footer.tsx b/src/components/layout/Footer.tsx
index ab4f57b..aa2647b 100644
--- a/src/components/layout/Footer.tsx
+++ b/src/components/layout/Footer.tsx
@@ -1,5 +1,6 @@
import Link from "next/link";
import { GitMerge } from "lucide-react";
+import { CopyrightYear } from "./CopyrightYear";
const columns = [
{
@@ -56,7 +57,9 @@ export function Footer() {
))}
-
© {new Date().getFullYear()} MergeFi. All rights reserved.
+
+ © MergeFi. All rights reserved.
+
Where open source meets finance.
From 5f27e73a7bf210ab750d787cd00e5a5685d0ffd0 Mon Sep 17 00:00:00 2001
From: miraclesonly
Date: Tue, 25 Aug 2026 09:05:28 +0100
Subject: [PATCH 3/4] fix(layout): add themeColor viewport metadata matching
light/dark backgrounds
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
viewport only set colorScheme, which controls native UI elements
(scrollbars, form controls) but not the mobile browser's own chrome —
the address/status bar background on iOS Safari and Android Chrome.
Neither this app's dark background (#0a0a0f) nor its light background
(#fbfbfd, both from globals.css) was ever passed as themeColor, so
mobile browsers fell back to their own default (usually white),
visibly clashing with the page the instant it loads — especially in
dark mode, where a white/default chrome bar sits directly above a
near-black page.
Added a themeColor array mirroring the same prefers-color-scheme split
already used in globals.css (closes #224).
---
src/app/layout.tsx | 9 +++++++++
1 file changed, 9 insertions(+)
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index 672b4ed..03a5767 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -25,6 +25,15 @@ export const metadata: Metadata = {
export const viewport: Viewport = {
colorScheme: "dark light",
+ // Matches the light/dark backgrounds in globals.css so mobile browser
+ // chrome (the address/status bar) doesn't clash with the page the
+ // instant it loads — without this, browsers fall back to their own
+ // default (usually white), which visibly contradicts a near-black dark
+ // mode page (#224).
+ themeColor: [
+ { media: "(prefers-color-scheme: light)", color: "#fbfbfd" },
+ { media: "(prefers-color-scheme: dark)", color: "#0a0a0f" },
+ ],
};
export default function RootLayout({
From ee8b7cd089f2dc0d70b4756f0e977aea9b909e49 Mon Sep 17 00:00:00 2001
From: miraclesonly
Date: Tue, 25 Aug 2026 09:05:39 +0100
Subject: [PATCH 4/4] fix(navbar): make the Dashboards dropdown reachable via
keyboard
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The "Dashboards" submenu was a CSS-only hover pattern
(group-hover:visible group-hover:opacity-100) with no focus/
focus-within variant anywhere, no onClick on the trigger, and no
aria-expanded/aria-haspopup. A keyboard user tabbing to the "Dashboards"
button never made the submenu visible — Tab skipped straight past it —
and the three links inside remained in the tab order (invisible/
opacity-0, not display: none) rather than being removed from it, so
focus could land on a link a keyboard user couldn't see was there at
all, worse than being skipped entirely.
Added group-focus-within:visible group-focus-within:opacity-100
alongside the existing hover variant, so focus landing anywhere inside
the wrapper (the trigger, or one of the links themselves once tabbed
to) keeps the menu open the same way hover does. Also added
aria-haspopup="menu" to the trigger for correct semantics (closes #222).
---
src/components/layout/Navbar.tsx | 15 +++++++++++++--
1 file changed, 13 insertions(+), 2 deletions(-)
diff --git a/src/components/layout/Navbar.tsx b/src/components/layout/Navbar.tsx
index b768467..eb29d9f 100644
--- a/src/components/layout/Navbar.tsx
+++ b/src/components/layout/Navbar.tsx
@@ -39,11 +39,22 @@ export function Navbar() {
))}
-
+
Dashboards
-
+ {/* group-focus-within alongside group-hover: the submenu was
+ only reachable via mouse hover — a keyboard user tabbing
+ to the trigger never made it visible, and the links
+ inside stayed in the tab order (invisible/opacity-0, not
+ display: none) so focus could land on an invisible link
+ (#222). focus-within keeps it open while focus is
+ anywhere inside this wrapper, including on the links
+ themselves. */}
+
{dashboardLinks.map((link) => (