fix(panel): active tab/route highlight (pickTab + sidebar footer exact match) (#520)

* feat(panel): add pickTab helper for validated URL tab params

* fix(panel): validate kanban view + KB tab params via pickTab

The bare `as T || default` cast only guarded null — a typo/invalid value
(?view=deev, ?tab=foo) passed through as an out-of-set TabValue, blanking the
active tab highlight and the content pane. pickTab validates against the
known set and falls back to the default on null/empty/invalid.

* fix(panel): highlight active sidebar footer link (exact match)

SidebarFooter had no isActive branch (SidebarNav does), so footer links
never highlighted. Added with EXACT match (pathname === href) — not
startsWith — so /settings does not also highlight on /settings/ai-providers.
Main nav keeps startsWith (longer hrefs need prefix matching); the two
intentionally differ.

---------

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
This commit is contained in:
Renzo F
2026-07-15 04:33:13 +02:00
committed by GitHub
co-authored by Renn F
parent ba9c9d69d8
commit 0d42232d9d
6 changed files with 87 additions and 6 deletions
+23
View File
@@ -0,0 +1,23 @@
import { describe, expect, it } from "vitest";
import { pickTab } from "@/lib/tabs";
const VIEWS = ["dev", "qa", "pr-review", "pm"] as const;
describe("pickTab", () => {
it("returns the raw value when it is in the valid set", () => {
expect(pickTab("qa", VIEWS, "dev")).toBe("qa");
});
it("falls back when raw is null", () => {
expect(pickTab(null, VIEWS, "dev")).toBe("dev");
});
it("falls back when raw is an invalid/typo value", () => {
expect(pickTab("deev", VIEWS, "dev")).toBe("dev");
expect(pickTab("foo", VIEWS, "dev")).toBe("dev");
});
it("falls back when raw is the empty string", () => {
expect(pickTab("", VIEWS, "dev")).toBe("dev");
});
});
+13
View File
@@ -0,0 +1,13 @@
/**
* Validate a URL search-param tab/view value against a known set, falling
* back to a default when it is null, empty, or not in the set. Replaces the
* bare `as T || default` cast that only guards null and lets a typo value
* blank the active tab + content pane.
*/
export function pickTab<T extends string>(
raw: string | null,
valid: readonly T[],
fallback: T,
): T {
return valid.includes(raw as T) ? (raw as T) : fallback;
}