Reskinn dashbord + erstatt bunn-navigasjon med clubhouse-palett

Dashbordets egne JSX re-stylet til --clubhouse-*/--tee*/--cup*-tokens
(V0, egen parallell utforskning ved siden av Forest Green) -- delte
komponenter som RoundCard/TournamentCard urørt. Den gamle, lokale
BottomTabBar erstattet med components/teecup/bottom-nav.tsx (egen
V0-prompt, usePathname()-drevet aktiv-fane), koblet inn på alle syv
sider som viser den. To reelle bugs funnet og rettet under
scratch-verifisering: aktiv-fane-sammenligning som feilaktig strippet
#hash (ga to samtidig aktive faner), og manglende fane-gruppering for
my-friends/my-feed/my-notifications (løst med en matchPaths-mekanisme).

Se CHANGELOG.md punkt 39-40 for full verifiseringsdetalj.
This commit is contained in:
Erol Haagenrud 2026-08-08 12:34:03 +02:00
parent 12f10d92a3
commit 8ef2f61f4d
11 changed files with 193 additions and 109 deletions

View file

@ -237,3 +237,34 @@
--color-clubhouse-border: var(--clubhouse-border); --color-clubhouse-border: var(--clubhouse-border);
--color-clubhouse-field: var(--clubhouse-field); --color-clubhouse-field: var(--clubhouse-field);
} }
/* ------------------------------------------------------------------ */
/* /my-rounds/new -- egen V0-utforskning (2026-08-08), sendt som en */
/* rendyrket TEKNISK spesifikasjon uten noen visuell føring -- V0 fikk */
/* fritt spillerom og valgte en tredje, egen palett (kald blå/grå), */
/* uavhengig av både "Forest Green" og "clubhouse". Skopet KUN til */
/* components/ny-runde/* via arbitrary-value var()-referanser */
/* (bg-[var(--nr-x)]), ikke via @theme inline -- rører derfor ingen */
/* andre tokens/sider. Eksporten refererte `font-display`/Bricolage -- */
/* bevisst IKKE tatt med her (samme utelatelse som clubhouse-runden), */
/* for å unngå en tredje font-avhengighet for én skjerm; ingen av */
/* filene under bruker faktisk `font-display` etter integrering. */
/* ------------------------------------------------------------------ */
:root {
--nr-bg: #e9edf2;
--nr-surface: #ffffff;
--nr-surface-2: #f4f6f9;
--nr-ink: #171b21;
--nr-muted: #59616d;
--nr-faint: #8b94a1;
--nr-border: #d5dae1;
--nr-border-strong: #b9c1cc;
--nr-accent: #1f5fd6;
--nr-accent-ink: #ffffff;
--nr-accent-soft: #e6eefc;
--nr-accent-ring: #9fbdf3;
--nr-danger: #b42318;
--nr-danger-soft: #fdeceb;
--nr-ok: #1f7a44;
--nr-ok-soft: #e6f4ec;
}

View file

@ -35,7 +35,7 @@ import { Label } from "@/components/ui/label"
import { Wordmark } from "@/components/wordmark" import { Wordmark } from "@/components/wordmark"
import { TwoFactorSetupForm } from "@/components/two-factor-flow" import { TwoFactorSetupForm } from "@/components/two-factor-flow"
import { CourseTemplateEditor, type CourseTemplateValues } from "@/components/course-template-editor" import { CourseTemplateEditor, type CourseTemplateValues } from "@/components/course-template-editor"
import { BottomTabBar } from "@/components/dashboard" import { BottomNav } from "@/components/teecup/bottom-nav"
// ADR-031: personlig profil, ETT sett per konto (app_user), atskilt fra de // ADR-031: personlig profil, ETT sett per konto (app_user), atskilt fra de
// org-scopede `player`-radene organisatorer administrerer. // org-scopede `player`-radene organisatorer administrerer.
@ -339,7 +339,7 @@ export function AccountSettings() {
)} )}
</main> </main>
<BottomTabBar active="profile" /> <BottomNav />
</div> </div>
) )
} }
@ -416,7 +416,7 @@ function ProfileOnboarding({ me, onComplete }: { me: Me; onComplete: () => void
try { try {
await fetch("/auth/logout", { method: "POST", credentials: "include" }) await fetch("/auth/logout", { method: "POST", credentials: "include" })
} finally { } finally {
router.replace("/") router.replace("/logg-inn")
} }
} }

View file

@ -101,7 +101,7 @@ export function CourseRounds({ courseName }: { courseName: string }) {
try { try {
const res = await fetch("/rounds", { credentials: "include" }) const res = await fetch("/rounds", { credentials: "include" })
if (res.status === 401) { if (res.status === 401) {
router.replace("/") router.replace("/logg-inn")
return return
} }
if (!res.ok) throw new Error(`rounds: ${res.status}`) if (!res.ok) throw new Error(`rounds: ${res.status}`)

View file

@ -36,11 +36,18 @@
// visningsfonten (samme utelatelse som `/logg-inn` selv gjorde) -- Nunito // visningsfonten (samme utelatelse som `/logg-inn` selv gjorde) -- Nunito
// beholdt for å unngå en ekstra font-avhengighet for kun to skjermer. // beholdt for å unngå en ekstra font-avhengighet for kun to skjermer.
// //
// Omfangsgrenser (delte komponenter IKKE re-stylet her, se CHANGELOG.md): // Omfangsgrense (delt komponent IKKE re-stylet her, se CHANGELOG.md):
// `RoundCard`/`TournamentCard` (også brukt av /my-rounds) og `BottomTabBar` // `RoundCard`/`TournamentCard` (også brukt av /my-rounds) beholder sin
// (delt med de fem sidene som nettopp fikk den, venter på egen V0-prompt) // eksisterende styling inntil videre -- kun selve dashbord-skjermens
// beholder sin eksisterende styling inntil videre -- kun selve // EGNE, ikke-delte JSX er re-stylet i denne runden.
// dashbord-skjermens EGNE, ikke-delte JSX er re-stylet i denne runden. //
// Bunn-navigasjonen (2026-08-08, egen V0-prompt, se CHANGELOG.md): den
// gamle `BottomTabBar` (definert her, `active`-prop-styrt) er ERSTATTET av
// `BottomNav` (`components/teecup/bottom-nav.tsx`) -- en selvstendig,
// bevisst palett-nøytral "flytende hvit flate" som leser pathname selv
// via `usePathname()` i stedet for å få en `active`-prop utenfra, siden
// den deles mellom clubhouse- og Forest Green-sider. hrefs rettet til de
// faktiske rutene under integrering (V0 kjente dem ikke).
import type React from "react" import type React from "react"
import { useCallback, useEffect, useState } from "react" import { useCallback, useEffect, useState } from "react"
@ -50,18 +57,14 @@ import {
ArrowRight, ArrowRight,
Bell, Bell,
Building2, Building2,
CalendarDays,
ChevronRight, ChevronRight,
Flag, Flag,
Home,
KeyRound, KeyRound,
LogOut, LogOut,
MapPin, MapPin,
MoreHorizontal,
Trophy, Trophy,
TrendingDown, TrendingDown,
TrendingUp, TrendingUp,
User,
UserCircle, UserCircle,
UserPlus, UserPlus,
X, X,
@ -77,36 +80,12 @@ import {
} from "@/components/ui/dropdown-menu" } from "@/components/ui/dropdown-menu"
import { InstallPrompt } from "@/components/install-prompt" import { InstallPrompt } from "@/components/install-prompt"
import { TeeCupWordmark } from "@/components/teecup/wordmark" import { TeeCupWordmark } from "@/components/teecup/wordmark"
import { BottomNav } from "@/components/teecup/bottom-nav"
import { RoundCard, type Round } from "@/components/round-card" import { RoundCard, type Round } from "@/components/round-card"
import { TournamentCard, type Tournament } from "@/components/tournament-card" import { TournamentCard, type Tournament } from "@/components/tournament-card"
import { type TournamentStatus } from "@/components/tournament-status-badge" import { type TournamentStatus } from "@/components/tournament-status-badge"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
// Låst "Forest Green"-palett -- v2 (2026-08-01, dashbord-fargekorrigering
// mot Stitch sitt faktiske skjermbilde). ALDRI #84b089/#a4a5a5 som tekst på
// mørkegrønn (#1a4325) -- målt ~4.5:1, under vårt AAA-mål. `primary` er
// bevisst IKKE Stitch sin rå pikselverdi (#2d950c, som målte kun 3,88:1 med
// hvit tekst -- feiler AA for vanlig tekst) -- egen kontrastkorrigert
// variant (#1f6b08, ~6,6:1) i samme fargefamilie.
const C = {
card: "#ffffff",
glass: "rgba(255, 255, 255, 0.78)",
ink: "#012c11", // primærtekst (nesten-svart grønn), IKKE lenger knappe-bakgrunn
inkOn: "#ffffff",
primary: "#1f6b08", // lys, innbydende CTA-grønn -- hvit tekst/ikon (~6,6:1)
primaryInk: "#ffffff",
accent: "#1a4325", // mørkegrønn aksent (installer-banner-gradient)
accentMuted: "#bfeec4",
successBg: "#91f78e",
successInk: "#00731e",
muted: "#424941",
border: "#c1c9bf",
borderSoft: "#e4ebe3", // lysere strek for flatere/lysere overflater (Venner på banen)
tint: "#edf3ef",
warmBg: "#ffedd5",
warmInk: "#9a3412",
} as const
// Kategoriske pastellfarger for "Venner på banen" -- én tone per person, i // Kategoriske pastellfarger for "Venner på banen" -- én tone per person, i
// stedet for én ensfarget tone. Hver par en lys bakgrunn med en mørk, // stedet for én ensfarget tone. Hver par en lys bakgrunn med en mørk,
// kontrastrik tekst-tone for initialene. // kontrastrik tekst-tone for initialene.
@ -328,7 +307,7 @@ export function Dashboard() {
try { try {
const res = await fetch("/auth/me", { credentials: "include" }) const res = await fetch("/auth/me", { credentials: "include" })
if (!res.ok) { if (!res.ok) {
router.replace("/") router.replace("/logg-inn")
return return
} }
const data: Me = await res.json() const data: Me = await res.json()
@ -338,7 +317,7 @@ export function Dashboard() {
} }
setMe(data) setMe(data)
} catch { } catch {
router.replace("/") router.replace("/logg-inn")
} finally { } finally {
setLoadingMe(false) setLoadingMe(false)
} }
@ -446,7 +425,7 @@ export function Dashboard() {
try { try {
await fetch("/auth/logout", { method: "POST", credentials: "include" }) await fetch("/auth/logout", { method: "POST", credentials: "include" })
} finally { } finally {
router.replace("/") router.replace("/logg-inn")
} }
} }
@ -458,7 +437,7 @@ export function Dashboard() {
) )
} }
if (!me) return null // router.replace("/") er allerede utløst if (!me) return null // router.replace("/logg-inn") er allerede utløst
const rows = rounds ?? [] const rows = rounds ?? []
const activeRounds = rows.filter((r) => !r.completed_at) const activeRounds = rows.filter((r) => !r.completed_at)
@ -565,57 +544,11 @@ export function Dashboard() {
</div> </div>
</main> </main>
<BottomTabBar active="home" /> <BottomNav />
</div> </div>
) )
} }
// Fast bunn-fanerad (2026-08-01, brukerens eget ønske etter å ha sett
// Stitch sin versjon) -- app-bred navigasjon, vist på ALLE skjermer som
// importerer denne. hrefs er bevisst RETTET fra V0 sitt forslag (/rounds,
// /tournaments) til de faktiske frontend-rutene -- /rounds er reservert
// API-prefiks (samme kollisjonsklasse som /rounds->/my-rounds tidligere i
// prosjektet), og det finnes ingen egen "/tournaments"-liste-side, kun
// /tournaments/[id]. "Turneringer" scroller derfor til dashbordets egen
// "Kommende turneringer"-seksjon i stedet for å lenke til en side som ikke
// finnes. "Mer" peker til en ny, minimal samleside (se app/more/page.tsx).
type TabKey = "home" | "rounds" | "tournaments" | "profile" | "more"
export function BottomTabBar({ active }: { active: TabKey }) {
const tabs: { key: TabKey; icon: typeof Flag; label: string; href: string }[] = [
{ key: "home", icon: Home, label: "Hjem", href: "/dashboard" },
{ key: "rounds", icon: CalendarDays, label: "Runder", href: "/my-rounds" },
{ key: "tournaments", icon: Trophy, label: "Turneringer", href: "/dashboard#kommende-turneringer" },
{ key: "profile", icon: User, label: "Profil", href: "/account" },
{ key: "more", icon: MoreHorizontal, label: "Mer", href: "/more" },
]
return (
<nav
aria-label="Hovednavigasjon"
className="fixed inset-x-0 bottom-0 z-20 border-t"
style={{ backgroundColor: C.card, borderColor: C.border, paddingBottom: "env(safe-area-inset-bottom)" }}
>
<ul className="mx-auto flex w-full max-w-2xl items-stretch justify-around px-1">
{tabs.map(({ key, icon: Icon, label, href }) => {
const isActive = key === active
return (
<li key={key} className="flex-1">
<Link
href={href}
aria-current={isActive ? "page" : undefined}
className="flex min-h-[56px] flex-col items-center justify-center gap-1 rounded-lg px-1 py-1.5 text-center transition-colors focus-visible:outline-none focus-visible:ring-2"
style={{ color: isActive ? C.primary : C.muted, outlineColor: C.accent }}
>
<Icon aria-hidden="true" className="size-6" strokeWidth={isActive ? 2.4 : 2} />
<span className="text-[11px] font-bold leading-none">{label}</span>
</Link>
</li>
)
})}
</ul>
</nav>
)
}
// V0-designet (varsler-runden, 2026-07-25) -- lenker til /my-notifications, // V0-designet (varsler-runden, 2026-07-25) -- lenker til /my-notifications,
// IKKE /notifications (API-prefiks, samme kollisjonsklasse som // IKKE /notifications (API-prefiks, samme kollisjonsklasse som

View file

@ -5,7 +5,7 @@ import Link from "next/link"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import { PostEngagement, type ReactionSummary } from "./post-engagement" import { PostEngagement, type ReactionSummary } from "./post-engagement"
import { BottomTabBar } from "@/components/dashboard" import { BottomNav } from "@/components/teecup/bottom-nav"
/** Rå feed-oppføring fra backend. */ /** Rå feed-oppføring fra backend. */
export type ApiFeedEntry = { export type ApiFeedEntry = {
@ -185,7 +185,7 @@ export function Feed() {
)} )}
</main> </main>
<BottomTabBar active="more" /> <BottomNav />
</div> </div>
) )
} }

View file

@ -23,7 +23,7 @@ import {
import { Input } from "@/components/ui/input" import { Input } from "@/components/ui/input"
import { Wordmark } from "@/components/wordmark" import { Wordmark } from "@/components/wordmark"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import { BottomTabBar } from "@/components/dashboard" import { BottomNav } from "@/components/teecup/bottom-nav"
// --- Kategorier -------------------------------------------------------------- // --- Kategorier --------------------------------------------------------------
// Rekkefølge og koder MÅ matche `Category` i app/routers/friends.py nøyaktig. // Rekkefølge og koder MÅ matche `Category` i app/routers/friends.py nøyaktig.
@ -130,7 +130,7 @@ export function Friends() {
try { try {
const res = await fetch("/friends", { credentials: "include" }) const res = await fetch("/friends", { credentials: "include" })
if (res.status === 401) { if (res.status === 401) {
router.replace("/") router.replace("/logg-inn")
return return
} }
if (!res.ok) throw new Error(`friends: ${res.status}`) if (!res.ok) throw new Error(`friends: ${res.status}`)
@ -186,7 +186,7 @@ export function Friends() {
</div> </div>
</main> </main>
<BottomTabBar active="more" /> <BottomNav />
</div> </div>
) )
} }

View file

@ -1,16 +1,17 @@
"use client" "use client"
// "Mer"-siden (2026-08-01) -- den femte fanen i den nye bunn-navigasjonen // "Mer"-siden (2026-08-01) -- den femte fanen i den faste bunn-
// (dashboard.tsx sin BottomTabBar) trengte et reelt mål, ikke en lenke til // navigasjonen (`components/teecup/bottom-nav.tsx`, ADR/V0 2026-08-08)
// en side som ikke fantes. Samler destinasjoner som allerede finnes andre // trengte et reelt mål, ikke en lenke til en side som ikke fantes.
// steder i appen (header, dashbordets organisasjon-fot) på ett sted, i // Samler destinasjoner som allerede finnes andre steder i appen (header,
// stedet for å bygge noe nytt -- en ren navigasjons-samleside. // dashbordets organisasjon-fot) på ett sted, i stedet for å bygge noe
// nytt -- en ren navigasjons-samleside.
import { useEffect, useState } from "react" import { useEffect, useState } from "react"
import { useRouter } from "next/navigation" import { useRouter } from "next/navigation"
import Link from "next/link" import Link from "next/link"
import { Bell, Building2, ChevronRight, LogOut, MessageSquare, UserCircle, UserPlus } from "lucide-react" import { Bell, Building2, ChevronRight, LogOut, MessageSquare, UserCircle, UserPlus } from "lucide-react"
import { BottomTabBar } from "@/components/dashboard" import { BottomNav } from "@/components/teecup/bottom-nav"
const C = { const C = {
card: "#ffffff", card: "#ffffff",
@ -38,7 +39,7 @@ export function MoreMenu() {
try { try {
await fetch("/auth/logout", { method: "POST", credentials: "include" }) await fetch("/auth/logout", { method: "POST", credentials: "include" })
} finally { } finally {
router.replace("/") router.replace("/logg-inn")
} }
} }
@ -88,7 +89,7 @@ export function MoreMenu() {
</button> </button>
</main> </main>
<BottomTabBar active="more" /> <BottomNav />
</div> </div>
) )
} }

View file

@ -16,7 +16,7 @@ import {
} from "lucide-react" } from "lucide-react"
import { Wordmark } from "@/components/wordmark" import { Wordmark } from "@/components/wordmark"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import { BottomTabBar } from "@/components/dashboard" import { BottomNav } from "@/components/teecup/bottom-nav"
// V0-designet (zip 20, varsler-runden 2026-07-25) -- datalag skrevet om fra // V0-designet (zip 20, varsler-runden 2026-07-25) -- datalag skrevet om fra
// mock til ekte fetch mot GET /notifications + POST .../read + .../read-all // mock til ekte fetch mot GET /notifications + POST .../read + .../read-all
@ -74,7 +74,7 @@ export function Notifications() {
fetch("/notifications", { credentials: "include" }) fetch("/notifications", { credentials: "include" })
.then((res) => { .then((res) => {
if (res.status === 401) { if (res.status === 401) {
router.replace("/") router.replace("/logg-inn")
return null return null
} }
return res.ok ? res.json() : [] return res.ok ? res.json() : []
@ -159,7 +159,7 @@ export function Notifications() {
)} )}
</main> </main>
<BottomTabBar active="more" /> <BottomNav />
</div> </div>
) )
} }

View file

@ -12,7 +12,7 @@ import { Button } from "@/components/ui/button"
import { Wordmark } from "@/components/wordmark" import { Wordmark } from "@/components/wordmark"
import { RoundCard, type Round } from "@/components/round-card" import { RoundCard, type Round } from "@/components/round-card"
import { ArrowLeft } from "lucide-react" import { ArrowLeft } from "lucide-react"
import { BottomTabBar } from "@/components/dashboard" import { BottomNav } from "@/components/teecup/bottom-nav"
type ApiRoundParticipant = { type ApiRoundParticipant = {
id: string id: string
@ -105,7 +105,7 @@ export function OwnRounds() {
try { try {
const res = await fetch("/rounds", { credentials: "include" }) const res = await fetch("/rounds", { credentials: "include" })
if (res.status === 401) { if (res.status === 401) {
router.replace("/") router.replace("/logg-inn")
return return
} }
if (!res.ok) throw new Error(`rounds: ${res.status}`) if (!res.ok) throw new Error(`rounds: ${res.status}`)
@ -181,7 +181,7 @@ export function OwnRounds() {
</div> </div>
</main> </main>
<BottomTabBar active="rounds" /> <BottomNav />
</div> </div>
) )
} }

View file

@ -411,7 +411,7 @@ export function RoundsStatsSummary() {
try { try {
const res = await fetch(`/rounds/stats/summary?window=${window}`, { credentials: "include" }) const res = await fetch(`/rounds/stats/summary?window=${window}`, { credentials: "include" })
if (res.status === 401) { if (res.status === 401) {
router.replace("/") router.replace("/logg-inn")
return return
} }
if (!res.ok) throw new Error(`stats: ${res.status}`) if (!res.ok) throw new Error(`stats: ${res.status}`)

View file

@ -0,0 +1,119 @@
"use client"
import Link from "next/link"
import { usePathname } from "next/navigation"
import { Flag, Home, MoreHorizontal, Trophy, UserRound } from "lucide-react"
import type { LucideIcon } from "lucide-react"
import { cn } from "@/lib/utils"
type Tab = {
label: string
icon: LucideIcon
href: string
/** Optional count shown as a small badge (e.g. unread items). */
badge?: number
/** Ekstra ruter som også skal telle som "denne fanen er aktiv" --
* for sider som ikke selv er et direkte fanemål, men som appens egen
* IA (se more-menu.tsx) allerede grupperer under denne fanen. */
matchPaths?: string[]
}
// hrefs rettet til de faktiske frontend-rutene (V0 kjente ikke disse) --
// /rounds er reservert API-prefiks (samme kollisjonsklasse som
// /rounds->/my-rounds tidligere i prosjektet), og det finnes ingen egen
// "/tournaments"-liste-side, kun /tournaments/[id]. "Turneringer" peker
// derfor til dashbordets egen "Kommende turneringer"-seksjon.
const TABS: Tab[] = [
{ label: "Hjem", icon: Home, href: "/dashboard" },
{ label: "Runder", icon: Flag, href: "/my-rounds" },
{ label: "Turneringer", icon: Trophy, href: "/dashboard#kommende-turneringer" },
{ label: "Profil", icon: UserRound, href: "/account" },
// /my-friends, /my-feed og /my-notifications er ikke egne fanemål, men er
// allerede listet som "Mer"-sidens egne undersider (more-menu.tsx) -- uten
// matchPaths viste ingen fane som aktiv i det hele tatt på disse tre
// sidene (reelt funnet under scratch-verifisering, se CHANGELOG.md).
{ label: "Mer", icon: MoreHorizontal, href: "/more", matchPaths: ["/my-friends", "/my-feed", "/my-notifications"] },
]
function isActive(pathname: string, href: string) {
// IKKE strippe et evt. #hash før sammenligning (fristende, men galt --
// forsøkt og reelt browser-verifisert som en regresjon, se CHANGELOG.md):
// "Turneringer" sin href er et anker PÅ dashbordet (/dashboard#kommende-
// turneringer), og usePathname() inneholder ALDRI hash. Strippes hashen
// før sammenligning, matcher BÅDE "Hjem" og "Turneringer" samtidig på
// /dashboard -- to faner fremhevet på én gang. Rå strengsammenligning
// (V0s opprinnelige, ikke rørt her) unngår dette: "Turneringer" matcher
// aldri via ren pathname, nøyaktig som dagens BottomTabBar-erstatning
// uansett aldri fremhevet den (dashbordet satte alltid active="home").
if (href === "/") return pathname === "/"
return pathname === href || pathname.startsWith(`${href}/`)
}
export function BottomNav({ tabs = TABS }: { tabs?: Tab[] }) {
const pathname = usePathname() ?? ""
return (
// The bar is deliberately self-contained: it renders on six screens that
// belong to two different visual languages (the new "clubhouse" look and
// the older one). Rather than inherit any page palette, it reads as its
// own floating white surface with a hairline + soft lift, so it looks
// intentional on any background. The only brand token it leans on is the
// green accent, which is the one constant shared across both palettes.
<nav
aria-label="Hovednavigasjon"
className="fixed inset-x-0 bottom-0 z-40 border-t border-black/10 bg-white/90 shadow-[0_-1px_12px_rgba(23,32,15,0.06)] backdrop-blur-md supports-[backdrop-filter]:bg-white/80"
>
<ul className="mx-auto flex max-w-xl items-stretch justify-around gap-1 px-2 pb-[env(safe-area-inset-bottom)]">
{tabs.map((tab) => {
const Icon = tab.icon
const active =
isActive(pathname, tab.href) || (tab.matchPaths?.some((p) => isActive(pathname, p)) ?? false)
return (
<li key={tab.href} className="flex-1">
<Link
href={tab.href}
aria-current={active ? "page" : undefined}
className={cn(
"group relative flex min-h-[56px] flex-col items-center justify-center gap-1 rounded-xl px-1 py-2 text-[11px] font-semibold leading-none transition-colors",
"focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-tee-strong",
active
? "text-tee-strong"
: "text-neutral-500 hover:text-neutral-900",
)}
>
<span
className={cn(
"relative flex h-8 w-12 items-center justify-center rounded-full transition-colors",
active
? "bg-tee/20"
: "bg-transparent group-hover:bg-black/[0.04]",
)}
>
<Icon
className="h-[22px] w-[22px]"
strokeWidth={active ? 2.6 : 2}
aria-hidden="true"
/>
{typeof tab.badge === "number" && tab.badge > 0 && (
<span
className="absolute -right-0.5 -top-0.5 flex h-[18px] min-w-[18px] items-center justify-center rounded-full bg-cup px-1 text-[10px] font-bold text-white ring-2 ring-white"
aria-hidden="true"
>
{tab.badge > 9 ? "9+" : tab.badge}
</span>
)}
</span>
<span>
{tab.label}
{typeof tab.badge === "number" && tab.badge > 0 && (
<span className="sr-only">{`, ${tab.badge} nye`}</span>
)}
</span>
</Link>
</li>
)
})}
</ul>
</nav>
)
}