ui: separate trackers from projects; keep header dropdown on top
- /trackers index, Trackers section on the homepage and in the header nav; /projects lists only projects (tracker URLs unchanged) - header is its own z-50 layer and <main> is isolated, so maps, charts and sticky tables no longer paint over the nav dropdowns
This commit is contained in:
@@ -7,7 +7,7 @@
|
||||
import { getSeo } from '#lib/seo.js';
|
||||
import CopyButton from '#lib/components/CopyButton.svelte';
|
||||
import ProjectIcon from '#lib/components/ProjectIcon.svelte';
|
||||
import { projects } from '#lib/projects.js';
|
||||
import { projects, trackers } from '#lib/projects.js';
|
||||
import { Button, buttonVariants } from '#lib/components/ui/button/index.js';
|
||||
import * as Sheet from '#lib/components/ui/sheet/index.js';
|
||||
import ArrowRight from 'lucide-svelte/icons/arrow-right';
|
||||
@@ -31,15 +31,24 @@
|
||||
let { data, children } = $props();
|
||||
const seo = $derived(getSeo(page.url.pathname, page.data, data.environment));
|
||||
let menuOpen = $state(false);
|
||||
let projectsMenuOpen = $state(false);
|
||||
// Which header dropdown is open (desktop): '/projects', '/trackers' or none.
|
||||
let openMenu = $state<string | null>(null);
|
||||
type ThemePreference = 'dark' | 'system' | 'light';
|
||||
let theme = $state<ThemePreference>('dark');
|
||||
const navigation = [
|
||||
{ label: 'home', href: '/' },
|
||||
{ label: 'projects', href: '/projects' },
|
||||
{ label: 'trackers', href: '/trackers' },
|
||||
{ label: 'blog', href: '/blog' },
|
||||
{ label: 'lists', href: '/lists' }
|
||||
] as const;
|
||||
const menus: Record<
|
||||
string,
|
||||
{ label: string; items: readonly (typeof projects | typeof trackers)[number][] }
|
||||
> = {
|
||||
'/projects': { label: 'projects', items: projects },
|
||||
'/trackers': { label: 'trackers', items: trackers }
|
||||
};
|
||||
const socialLinks = [
|
||||
{ icon: SiMail, label: 'email', href: 'mailto:stan@stanrunge.dev' },
|
||||
{ icon: SiGithub, label: 'github', href: 'https://github.com/stanrunge' },
|
||||
@@ -102,14 +111,14 @@
|
||||
return page.url.pathname === href || (href !== '/' && page.url.pathname.startsWith(href + '/'));
|
||||
}
|
||||
|
||||
function handleProjectsPointerEnter(event: PointerEvent) {
|
||||
function handleMenuPointerEnter(event: PointerEvent, href: string) {
|
||||
if (event.pointerType === 'touch') return;
|
||||
projectsMenuOpen = true;
|
||||
openMenu = href;
|
||||
}
|
||||
|
||||
function handleProjectsPointerLeave(event: PointerEvent) {
|
||||
function handleMenuPointerLeave(event: PointerEvent) {
|
||||
if (event.pointerType === 'touch') return;
|
||||
projectsMenuOpen = false;
|
||||
openMenu = null;
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -170,7 +179,14 @@
|
||||
class="sr-only focus:not-sr-only focus:absolute focus:z-50 focus:bg-[var(--site-bg)] focus:px-4 focus:py-2"
|
||||
>skip to content</a
|
||||
>
|
||||
<header class="border-b border-border bg-background/95 backdrop-blur">
|
||||
<!--
|
||||
Stacking: backdrop-blur makes the header its own stacking context, so the dropdown's z-50 only
|
||||
counts inside it. Without a z-index of its own the header then sits at z-auto/0 and anything
|
||||
positioned later in <main> (maps, chart tooltips, sticky headers) paints over the dropdown.
|
||||
Fix: lift the header (relative z-50) and isolate <main> so page z-indexes stay inside it.
|
||||
Dialogs/sheets/popovers are portalled to <body> and still render above both.
|
||||
-->
|
||||
<header class="relative z-50 border-b border-border bg-background/95 backdrop-blur">
|
||||
<div class="mx-auto flex max-w-6xl items-center gap-3 px-4 py-3 sm:px-6">
|
||||
<a
|
||||
href={resolve('/')}
|
||||
@@ -190,39 +206,40 @@
|
||||
</a>
|
||||
<nav class="ml-auto hidden items-center gap-1 md:flex" aria-label="Main navigation">
|
||||
{#each navigation as item (item.href)}
|
||||
{#if item.href === '/projects'}
|
||||
{#if menus[item.href]}
|
||||
{@const menu = menus[item.href]}
|
||||
<div
|
||||
class="relative"
|
||||
role="group"
|
||||
aria-label="Projects menu"
|
||||
onpointerenter={handleProjectsPointerEnter}
|
||||
onpointerleave={handleProjectsPointerLeave}
|
||||
aria-label={`${menu.label} menu`}
|
||||
onpointerenter={(event) => handleMenuPointerEnter(event, item.href)}
|
||||
onpointerleave={handleMenuPointerLeave}
|
||||
>
|
||||
<Button
|
||||
href={resolve('projects')}
|
||||
href={resolve(item.href)}
|
||||
variant={current(item.href) ? 'secondary' : 'ghost'}
|
||||
size="sm"
|
||||
class="cursor-pointer gap-1"
|
||||
aria-label="Browse projects"
|
||||
aria-label={`Browse ${menu.label}`}
|
||||
aria-haspopup="true"
|
||||
aria-expanded={projectsMenuOpen}
|
||||
onclick={() => (projectsMenuOpen = false)}
|
||||
aria-expanded={openMenu === item.href}
|
||||
onclick={() => (openMenu = null)}
|
||||
onkeydown={(event) => {
|
||||
if (event.key === 'Escape') projectsMenuOpen = false;
|
||||
if (event.key === 'Escape') openMenu = null;
|
||||
}}>{item.label}<ChevronDown size={15} /></Button
|
||||
>
|
||||
{#if projectsMenuOpen}
|
||||
{#if openMenu === item.href}
|
||||
<div
|
||||
class="absolute top-full left-0 z-50 max-h-[70vh] w-72 overflow-y-auto rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-md"
|
||||
aria-label="Projects"
|
||||
aria-label={menu.label}
|
||||
>
|
||||
<a
|
||||
href={resolve('projects')}
|
||||
href={resolve(item.href)}
|
||||
class="flex items-center rounded-sm px-2 py-1.5 text-sm font-medium outline-none hover:bg-accent hover:text-accent-foreground focus-visible:bg-accent"
|
||||
>all projects <ArrowRight size={14} class="ml-auto" /></a
|
||||
>all {menu.label} <ArrowRight size={14} class="ml-auto" /></a
|
||||
>
|
||||
<div class="my-1 h-px bg-border"></div>
|
||||
{#each projects as project (project.name)}
|
||||
{#each menu.items as project (project.name)}
|
||||
<!-- Internal project links use resolve; external URLs are from the project catalog. -->
|
||||
<!-- eslint-disable svelte/no-navigation-without-resolve -->
|
||||
<a
|
||||
@@ -297,9 +314,9 @@
|
||||
class="justify-start"
|
||||
onclick={() => (menuOpen = false)}>{item.label}</Button
|
||||
>
|
||||
{#if item.href === '/projects'}
|
||||
{#if menus[item.href]}
|
||||
<div class="ml-3 flex flex-col border-l border-border pl-3">
|
||||
{#each projects as project (project.name)}
|
||||
{#each menus[item.href].items as project (project.name)}
|
||||
<Button
|
||||
href={project.external ? project.href : resolve(project.href)}
|
||||
variant="ghost"
|
||||
@@ -343,7 +360,7 @@
|
||||
</Sheet.Root>
|
||||
</div>
|
||||
</header>
|
||||
<main id="main" class="min-w-0 flex-1">{@render children()}</main>
|
||||
<main id="main" class="isolate min-w-0 flex-1">{@render children()}</main>
|
||||
<footer
|
||||
class="mt-16 border-t border-[var(--site-border)]"
|
||||
class:tracker-footer={page.route.id === '/projects/game-tracker'}
|
||||
|
||||
Reference in New Issue
Block a user