ui changes and cs prices
All checks were successful
Test and deploy staging / staging (push) Successful in 11m52s
All checks were successful
Test and deploy staging / staging (push) Successful in 11m52s
This commit is contained in:
@@ -6,9 +6,14 @@
|
||||
import { page } from '$app/state';
|
||||
import { getSeo } from '$lib/seo';
|
||||
import CopyButton from '$lib/components/CopyButton.svelte';
|
||||
import ProjectIcon from '$lib/components/ProjectIcon.svelte';
|
||||
import { projects } from '$lib/projects';
|
||||
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';
|
||||
import ChevronDown from 'lucide-svelte/icons/chevron-down';
|
||||
import ExternalLink from 'lucide-svelte/icons/external-link';
|
||||
import Menu from 'lucide-svelte/icons/menu';
|
||||
import SiOsu from '~icons/simple-icons/osu';
|
||||
import SiSteam from '~icons/simple-icons/steam';
|
||||
import SiTwitch from '~icons/simple-icons/twitch';
|
||||
@@ -26,6 +31,7 @@
|
||||
let { data, children } = $props();
|
||||
const seo = $derived(getSeo(page.url.pathname, page.data, data.environment));
|
||||
let menuOpen = $state(false);
|
||||
let projectsMenuOpen = $state(false);
|
||||
type ThemePreference = 'dark' | 'system' | 'light';
|
||||
let theme = $state<ThemePreference>('dark');
|
||||
const navigation = [
|
||||
@@ -76,6 +82,7 @@
|
||||
: preference;
|
||||
document.documentElement.dataset.themePreference = preference;
|
||||
document.documentElement.dataset.theme = resolvedTheme;
|
||||
document.documentElement.classList.toggle('dark', resolvedTheme === 'dark');
|
||||
document
|
||||
.querySelector('meta[name="theme-color"]')
|
||||
?.setAttribute('content', resolvedTheme === 'light' ? '#ffffff' : '#000000');
|
||||
@@ -94,6 +101,16 @@
|
||||
function current(href: string) {
|
||||
return page.url.pathname === href || (href !== '/' && page.url.pathname.startsWith(href + '/'));
|
||||
}
|
||||
|
||||
function handleProjectsPointerEnter(event: PointerEvent) {
|
||||
if (event.pointerType === 'touch') return;
|
||||
projectsMenuOpen = true;
|
||||
}
|
||||
|
||||
function handleProjectsPointerLeave(event: PointerEvent) {
|
||||
if (event.pointerType === 'touch') return;
|
||||
projectsMenuOpen = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
{#snippet themeIcon()}
|
||||
@@ -153,183 +170,177 @@
|
||||
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-[var(--site-border)]">
|
||||
<div class="mx-auto flex max-w-6xl items-center gap-5 px-4 py-4 sm:px-6">
|
||||
<header class="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('/')}
|
||||
class="flex shrink-0 items-center gap-3 text-base font-semibold tracking-tight"
|
||||
onclick={() => (menuOpen = false)}
|
||||
class="flex shrink-0 items-center gap-3 font-semibold tracking-tight"
|
||||
aria-label="Stan Runge home"
|
||||
>
|
||||
<span class="h-7 w-7 overflow-hidden rounded-full bg-black">
|
||||
<img
|
||||
<span class="h-7 w-7 overflow-hidden rounded-full bg-black"
|
||||
><img
|
||||
src="{assets}/favicon-32x32.png"
|
||||
alt=""
|
||||
width="32"
|
||||
height="32"
|
||||
class="h-full w-full scale-[1.7]"
|
||||
/>
|
||||
</span>
|
||||
/></span
|
||||
>
|
||||
<span>stan runge</span>
|
||||
</a>
|
||||
<nav class="ml-auto hidden items-center gap-6 text-base md:flex" aria-label="Main navigation">
|
||||
<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'}
|
||||
<div class="group relative">
|
||||
<a
|
||||
href={resolve(item.href)}
|
||||
aria-current={current(item.href) ? 'page' : undefined}
|
||||
<div
|
||||
class="relative"
|
||||
role="group"
|
||||
aria-label="Projects menu"
|
||||
onpointerenter={handleProjectsPointerEnter}
|
||||
onpointerleave={handleProjectsPointerLeave}
|
||||
>
|
||||
<Button
|
||||
href={resolve('/projects')}
|
||||
variant={current(item.href) ? 'secondary' : 'ghost'}
|
||||
size="sm"
|
||||
class="cursor-pointer gap-1"
|
||||
aria-label="Browse projects"
|
||||
aria-haspopup="true"
|
||||
class="inline-flex items-center gap-1 {current(item.href)
|
||||
? 'text-[var(--site-text)]'
|
||||
: 'text-[var(--site-muted)] hover:text-[var(--site-text)]'}"
|
||||
>{item.label}
|
||||
<ChevronDown
|
||||
size={14}
|
||||
aria-hidden="true"
|
||||
class="transition-transform group-focus-within:rotate-180 group-hover:rotate-180"
|
||||
/></a
|
||||
aria-expanded={projectsMenuOpen}
|
||||
onclick={() => (projectsMenuOpen = false)}
|
||||
onkeydown={(event) => {
|
||||
if (event.key === 'Escape') projectsMenuOpen = false;
|
||||
}}>{item.label}<ChevronDown size={15} /></Button
|
||||
>
|
||||
<div
|
||||
class="invisible absolute top-full left-1/2 z-50 -translate-x-1/2 pt-3 opacity-0 transition-opacity group-focus-within:visible group-focus-within:opacity-100 group-hover:visible group-hover:opacity-100"
|
||||
>
|
||||
<ul
|
||||
class="w-72 rounded-md border border-[var(--site-border)] bg-[var(--site-bg)] py-2 shadow-lg"
|
||||
{#if projectsMenuOpen}
|
||||
<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"
|
||||
>
|
||||
<a
|
||||
href={resolve('/projects')}
|
||||
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
|
||||
>
|
||||
<div class="my-1 h-px bg-border"></div>
|
||||
{#each projects as project (project.name)}
|
||||
<li>
|
||||
{#if project.external}
|
||||
<a
|
||||
href={project.href}
|
||||
rel="external"
|
||||
class="flex items-center justify-between gap-3 px-4 py-2 hover:bg-[var(--site-surface)] focus-visible:bg-[var(--site-surface)]"
|
||||
>
|
||||
<span class="min-w-0">
|
||||
<span class="block text-[var(--site-text)]">{project.name}</span>
|
||||
<span class="block truncate text-sm text-[var(--site-muted)]"
|
||||
>{project.subtitle}</span
|
||||
>
|
||||
</span><ExternalLink
|
||||
size={14}
|
||||
aria-hidden="true"
|
||||
class="shrink-0 text-[var(--site-muted)]"
|
||||
/>
|
||||
</a>
|
||||
{:else}
|
||||
<a
|
||||
href={resolve(project.href)}
|
||||
aria-current={current(project.href) ? 'page' : undefined}
|
||||
class="block px-4 py-2 hover:bg-[var(--site-surface)] focus-visible:bg-[var(--site-surface)]"
|
||||
>
|
||||
<span class="block text-[var(--site-text)]">{project.name}</span>
|
||||
<span class="block truncate text-sm text-[var(--site-muted)]"
|
||||
>{project.subtitle}</span
|
||||
>
|
||||
</a>
|
||||
{/if}
|
||||
</li>
|
||||
<a
|
||||
href={project.external ? project.href : resolve(project.href)}
|
||||
rel={project.external ? 'external' : undefined}
|
||||
class="flex items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none hover:bg-accent hover:text-accent-foreground focus-visible:bg-accent"
|
||||
>
|
||||
<span
|
||||
class="flex size-8 shrink-0 items-center justify-center rounded border border-border bg-[var(--site-surface)] text-[var(--site-muted)]"
|
||||
>
|
||||
<ProjectIcon name={project.name} class="size-4" />
|
||||
</span>
|
||||
<span class="min-w-0"
|
||||
><span class="block font-medium">{project.name}</span><span
|
||||
class="block truncate text-xs text-muted-foreground"
|
||||
>{project.subtitle}</span
|
||||
></span
|
||||
>
|
||||
{#if project.external}<ExternalLink size={14} class="ml-auto shrink-0" />{/if}
|
||||
</a>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<a
|
||||
<Button
|
||||
href={resolve(item.href)}
|
||||
aria-current={current(item.href) ? 'page' : undefined}
|
||||
class={current(item.href)
|
||||
? 'text-[var(--site-text)]'
|
||||
: 'text-[var(--site-muted)] hover:text-[var(--site-text)]'}>{item.label}</a
|
||||
variant={current(item.href) ? 'secondary' : 'ghost'}
|
||||
size="sm">{item.label}</Button
|
||||
>
|
||||
{/if}
|
||||
{/each}
|
||||
</nav>
|
||||
<div class="ml-auto hidden items-center gap-5 text-base md:ml-4 md:flex">
|
||||
<button
|
||||
type="button"
|
||||
<div class="hidden items-center gap-1 md:flex">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onclick={toggleTheme}
|
||||
aria-label={`Theme: ${theme}. Switch to ${theme === 'dark' ? 'system' : theme === 'system' ? 'light' : 'dark'} mode`}
|
||||
title={`Theme: ${theme}`}
|
||||
class="flex h-10 w-10 items-center justify-center text-[var(--site-muted)] hover:text-[var(--site-text)]"
|
||||
>{@render themeIcon()}</button
|
||||
aria-label={`Theme: ${theme}. Switch mode`}
|
||||
title={`Theme: ${theme}`}>{@render themeIcon()}</Button
|
||||
>
|
||||
{#if data.user}
|
||||
<a href={resolve('/account')} class="max-w-28 truncate hover:underline"
|
||||
>{data.user.name}</a
|
||||
<Button href={resolve('/account')} variant="outline" size="sm" class="max-w-36 truncate"
|
||||
>{data.user.name}</Button
|
||||
>
|
||||
{:else}
|
||||
<a
|
||||
<Button
|
||||
href={resolve(
|
||||
`/login?returnTo=${encodeURIComponent(page.url.pathname + page.url.search)}`
|
||||
)}
|
||||
class="hover:underline">sign in</a
|
||||
variant="outline"
|
||||
size="sm">sign in</Button
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="ml-auto text-base text-[var(--site-muted)] md:hidden"
|
||||
aria-label={menuOpen ? 'Close menu' : 'Open menu'}
|
||||
aria-expanded={menuOpen}
|
||||
aria-controls="mobile-menu"
|
||||
onclick={() => (menuOpen = !menuOpen)}>{menuOpen ? 'close' : 'menu'}</button
|
||||
>
|
||||
</div>
|
||||
{#if menuOpen}
|
||||
<nav
|
||||
id="mobile-menu"
|
||||
class="border-t border-[var(--site-border)] px-4 py-4 md:hidden"
|
||||
aria-label="Mobile navigation"
|
||||
>
|
||||
<div class="mx-auto flex max-w-6xl flex-col items-center gap-1 text-center text-base">
|
||||
{#each navigation as item (item.href)}
|
||||
<a href={resolve(item.href)} onclick={() => (menuOpen = false)} class="w-full py-2"
|
||||
>{item.label}</a
|
||||
>
|
||||
{#if item.href === '/projects'}
|
||||
<ul class="mb-2 flex w-full flex-col text-[var(--site-muted)]">
|
||||
{#each projects as project (project.name)}
|
||||
<li>
|
||||
{#if project.external}
|
||||
<a
|
||||
href={project.href}
|
||||
rel="external"
|
||||
onclick={() => (menuOpen = false)}
|
||||
class="inline-flex items-center gap-1 py-1.5 hover:text-[var(--site-text)]"
|
||||
>{project.name} <ExternalLink size={12} aria-hidden="true" /></a
|
||||
>
|
||||
{:else}
|
||||
<a
|
||||
href={resolve(project.href)}
|
||||
onclick={() => (menuOpen = false)}
|
||||
class="block py-1.5 hover:text-[var(--site-text)]">{project.name}</a
|
||||
>
|
||||
{/if}
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
{/each}
|
||||
<div
|
||||
class="mt-3 flex w-full items-center justify-center gap-5 border-t border-[var(--site-border)] pt-4"
|
||||
<Sheet.Root bind:open={menuOpen}>
|
||||
<Sheet.Trigger
|
||||
class={buttonVariants({ variant: 'ghost', size: 'icon' }) + ' ml-auto md:hidden'}
|
||||
aria-label="Open navigation"><Menu size={20} /></Sheet.Trigger
|
||||
>
|
||||
<Sheet.Content side="right" class="overflow-y-auto p-6">
|
||||
<Sheet.Header
|
||||
><Sheet.Title>stan runge</Sheet.Title><Sheet.Description>Navigation</Sheet.Description
|
||||
></Sheet.Header
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
<nav class="flex flex-col gap-1" aria-label="Mobile navigation">
|
||||
{#each navigation as item (item.href)}
|
||||
<Button
|
||||
href={resolve(item.href)}
|
||||
variant={current(item.href) ? 'secondary' : 'ghost'}
|
||||
class="justify-start"
|
||||
onclick={() => (menuOpen = false)}>{item.label}</Button
|
||||
>
|
||||
{#if item.href === '/projects'}
|
||||
<div class="ml-3 flex flex-col border-l border-border pl-3">
|
||||
{#each projects as project (project.name)}
|
||||
<Button
|
||||
href={project.external ? project.href : resolve(project.href)}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="justify-start"
|
||||
onclick={() => (menuOpen = false)}>{project.name}</Button
|
||||
>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
</nav>
|
||||
<Sheet.Footer
|
||||
class="mt-auto flex-row items-center justify-between border-t border-border pt-4"
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onclick={toggleTheme}
|
||||
aria-label={`Theme: ${theme}. Switch to ${theme === 'dark' ? 'system' : theme === 'system' ? 'light' : 'dark'} mode`}
|
||||
title={`Theme: ${theme}`}
|
||||
class="flex h-10 w-10 items-center justify-center text-[var(--site-muted)]"
|
||||
>{@render themeIcon()}</button
|
||||
aria-label={`Theme: ${theme}. Switch mode`}>{@render themeIcon()}</Button
|
||||
>
|
||||
{#if data.user}
|
||||
<a href={resolve('/account')} onclick={() => (menuOpen = false)}>account</a>
|
||||
<form method="POST" action="/logout"><button>sign out</button></form>
|
||||
<Button
|
||||
href={resolve('/account')}
|
||||
variant="outline"
|
||||
onclick={() => (menuOpen = false)}>account</Button
|
||||
>
|
||||
<form method="POST" action="/logout">
|
||||
<Button type="submit" variant="outline">sign out</Button>
|
||||
</form>
|
||||
{:else}
|
||||
<a href={resolve('/login')} onclick={() => (menuOpen = false)}>sign in</a>
|
||||
<a href={resolve('/register')} onclick={() => (menuOpen = false)}>register</a>
|
||||
<Button href={resolve('/login')} variant="outline" onclick={() => (menuOpen = false)}
|
||||
>sign in</Button
|
||||
>
|
||||
<Button
|
||||
href={resolve('/register')}
|
||||
variant="outline"
|
||||
onclick={() => (menuOpen = false)}>register</Button
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
{/if}
|
||||
</Sheet.Footer>
|
||||
</Sheet.Content>
|
||||
</Sheet.Root>
|
||||
</div>
|
||||
</header>
|
||||
<main id="main" class="min-w-0 flex-1">{@render children()}</main>
|
||||
<footer class="mt-16 border-t border-[var(--site-border)]">
|
||||
@@ -349,13 +360,13 @@
|
||||
</div>
|
||||
<nav class="flex flex-wrap items-center justify-center gap-1" aria-label="Social links">
|
||||
{#each socialLinks as { icon: Icon, label, href } (label)}
|
||||
<a
|
||||
<Button
|
||||
{href}
|
||||
variant="ghost"
|
||||
size="icon-lg"
|
||||
rel="external"
|
||||
aria-label={label}
|
||||
title={label}
|
||||
class="flex h-10 w-10 items-center justify-center text-xl hover:text-[var(--site-text)]"
|
||||
><Icon /></a
|
||||
title={label}><Icon class="size-6" /></Button
|
||||
>
|
||||
{/each}
|
||||
{#each copyLinks as item (item.label)}<CopyButton {...item} />{/each}
|
||||
|
||||
Reference in New Issue
Block a user