books: immersive reader route, PDF resume fix, scalable placeholder covers, read-only books mount in prod deploy, README

This commit is contained in:
2026-10-24 01:18:17 +00:00
parent 9c46e281c2
commit 9c8f20a618
5 changed files with 268 additions and 211 deletions

View File

@@ -51,6 +51,7 @@
aspect-ratio: 2 / 3;
width: 100%;
overflow: hidden;
container-type: inline-size;
border-radius: 0.375rem 0.625rem 0.625rem 0.375rem;
background: var(--site-hover);
box-shadow:
@@ -99,31 +100,26 @@
font-family: Georgia, 'Times New Roman', serif;
font-weight: 600;
line-height: 1.15;
font-size: 1rem;
/* Scale with the cover's width so thumbnails don't break words mid-way. */
font-size: clamp(0.55rem, 12cqi, 1.6rem);
display: -webkit-box;
-webkit-line-clamp: 6;
line-clamp: 6;
-webkit-box-orient: vertical;
overflow: hidden;
overflow-wrap: anywhere;
overflow-wrap: break-word;
hyphens: auto;
}
.g-author {
font-size: 0.7rem;
font-size: clamp(0.5rem, 6.5cqi, 0.85rem);
letter-spacing: 0.04em;
text-transform: uppercase;
opacity: 0.8;
}
.cover-sm .g-title {
font-size: 0.6rem;
}
.cover-sm .g-author {
display: none;
}
.cover-lg .g-title {
font-size: 1.5rem;
}
.cover-lg .g-author {
font-size: 0.85rem;
@container (max-width: 90px) {
.g-author {
display: none;
}
}
.is-placeholder {
box-shadow: none;

View File

@@ -127,6 +127,9 @@
let lastScale = 0;
$effect(() => {
const s = scale;
// The first scale with pages loaded is the baseline; the initial jump to the saved
// position happens in onMount and must not be undone by a "keep position" here.
if (!pages.length || !width) return;
if (!lastScale) {
lastScale = s;
return;

View File

@@ -30,6 +30,8 @@
let { data, children } = $props();
const seo = $derived(getSeo(page.url.pathname, page.data, data.environment));
// The book reader is a full-screen view; the site header and footer would sit on top of it.
const immersive = $derived(/^\/trackers\/books\/\d+\/read\/?$/.test(page.url.pathname));
let menuOpen = $state(false);
// Which header dropdown is open (desktop): '/projects', '/trackers' or none.
let openMenu = $state<string | null>(null);
@@ -184,215 +186,224 @@
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('/')}
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
src={asset('favicon-32x32.png')}
alt=""
width="32"
height="32"
class="h-full w-full scale-[1.7]"
/></span
{#if !immersive}
<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('/')}
class="flex shrink-0 items-center gap-3 font-semibold tracking-tight"
aria-label="Stan Runge home"
>
<span>stan runge</span>
</a>
<nav class="ml-auto hidden items-center gap-1 md:flex" aria-label="Main navigation">
{#each navigation as item (item.href)}
{#if menus[item.href]}
{@const menu = menus[item.href]}
<div
class="relative"
role="group"
aria-label={`${menu.label} menu`}
onpointerenter={(event) => handleMenuPointerEnter(event, item.href)}
onpointerleave={handleMenuPointerLeave}
>
<Button
href={resolve(item.href)}
variant={current(item.href) ? 'secondary' : 'ghost'}
size="sm"
class="cursor-pointer gap-1"
aria-label={`Browse ${menu.label}`}
aria-haspopup="true"
aria-expanded={openMenu === item.href}
onclick={() => (openMenu = null)}
onkeydown={(event) => {
if (event.key === 'Escape') openMenu = null;
}}>{item.label}<ChevronDown size={15} /></Button
<span class="h-7 w-7 overflow-hidden rounded-full bg-black"
><img
src={asset('favicon-32x32.png')}
alt=""
width="32"
height="32"
class="h-full w-full scale-[1.7]"
/></span
>
<span>stan runge</span>
</a>
<nav class="ml-auto hidden items-center gap-1 md:flex" aria-label="Main navigation">
{#each navigation as item (item.href)}
{#if menus[item.href]}
{@const menu = menus[item.href]}
<div
class="relative"
role="group"
aria-label={`${menu.label} menu`}
onpointerenter={(event) => handleMenuPointerEnter(event, item.href)}
onpointerleave={handleMenuPointerLeave}
>
{#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={menu.label}
<Button
href={resolve(item.href)}
variant={current(item.href) ? 'secondary' : 'ghost'}
size="sm"
class="cursor-pointer gap-1"
aria-label={`Browse ${menu.label}`}
aria-haspopup="true"
aria-expanded={openMenu === item.href}
onclick={() => (openMenu = null)}
onkeydown={(event) => {
if (event.key === 'Escape') openMenu = null;
}}>{item.label}<ChevronDown size={15} /></Button
>
<a
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 {menu.label} <ArrowRight size={14} class="ml-auto" /></a
{#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={menu.label}
>
<div class="my-1 h-px bg-border"></div>
{#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
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"
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 {menu.label} <ArrowRight size={14} class="ml-auto" /></a
>
<span
class="flex size-8 shrink-0 items-center justify-center rounded border border-border bg-[var(--site-surface)] text-[var(--site-muted)]"
<div class="my-1 h-px bg-border"></div>
{#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
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"
>
<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>
<!-- eslint-enable svelte/no-navigation-without-resolve -->
{/each}
</div>
{/if}
</div>
{:else}
<Button
href={resolve(item.href)}
variant={current(item.href) ? 'secondary' : 'ghost'}
size="sm">{item.label}</Button
>
{/if}
{/each}
</nav>
<div class="hidden items-center gap-1 md:flex">
<Button
variant="ghost"
size="icon"
onclick={toggleTheme}
aria-label={`Theme: ${theme}. Switch mode`}
title={`Theme: ${theme}`}>{@render themeIcon()}</Button
>
{#if data.user}
<Button href={resolve('account')} variant="outline" size="sm" class="max-w-36 truncate"
>{data.user.name}</Button
>
{:else}
<Button
href={resolve(
`login?returnTo=${encodeURIComponent(page.url.pathname + page.url.search)}`
)}
variant="outline"
size="sm">sign in</Button
>
{/if}
</div>
<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
>
<nav class="flex flex-col gap-1" aria-label="Mobile navigation">
{#each navigation as item (item.href)}
<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>
<!-- eslint-enable svelte/no-navigation-without-resolve -->
{/each}
</div>
{/if}
</div>
{:else}
<Button
href={resolve(item.href)}
variant={current(item.href) ? 'secondary' : 'ghost'}
class="justify-start"
onclick={() => (menuOpen = false)}>{item.label}</Button
>
{#if menus[item.href]}
<div class="ml-3 flex flex-col border-l border-border pl-3">
{#each menus[item.href].items 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 mode`}>{@render themeIcon()}</Button
>
{#if data.user}
<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}
<Button href={resolve('login')} variant="outline" onclick={() => (menuOpen = false)}
>sign in</Button
>
<Button
href={resolve('register')}
variant="outline"
onclick={() => (menuOpen = false)}>register</Button
size="sm">{item.label}</Button
>
{/if}
</Sheet.Footer>
</Sheet.Content>
</Sheet.Root>
</div>
</header>
<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'}
class:cod-footer={page.route.id === '/projects/game-tracker/call-of-duty/[slug]'}
>
<div class="mx-auto max-w-6xl space-y-5 px-4 py-8 text-[var(--site-muted)] sm:px-6">
<div class="flex flex-wrap items-center justify-between gap-3 text-base">
<span
>stan runge <span class="ml-2 font-mono text-xs" title="Deployed commit"
>{data.version.slice(0, 7)}</span
></span
>
<a
href="https://git.stanrunge.dev/stan/website"
rel="external"
class="inline-flex items-center gap-1 hover:text-[var(--site-text)]"
>source <ExternalLink size={14} aria-hidden="true" /></a
>
</div>
<nav class="flex flex-wrap items-center justify-center gap-1" aria-label="Social links">
{#each socialLinks as { icon: Icon, label, href } (label)}
{/each}
</nav>
<div class="hidden items-center gap-1 md:flex">
<Button
{href}
variant="ghost"
size="icon-lg"
rel="external"
aria-label={label}
title={label}><Icon class="size-6" /></Button
size="icon"
onclick={toggleTheme}
aria-label={`Theme: ${theme}. Switch mode`}
title={`Theme: ${theme}`}>{@render themeIcon()}</Button
>
{/each}
{#each copyLinks as item (item.label)}<CopyButton {...item} />{/each}
</nav>
</div>
</footer>
{#if data.user}
<Button href={resolve('account')} variant="outline" size="sm" class="max-w-36 truncate"
>{data.user.name}</Button
>
{:else}
<Button
href={resolve(
`login?returnTo=${encodeURIComponent(page.url.pathname + page.url.search)}`
)}
variant="outline"
size="sm">sign in</Button
>
{/if}
</div>
<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
>
<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 menus[item.href]}
<div class="ml-3 flex flex-col border-l border-border pl-3">
{#each menus[item.href].items 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 mode`}>{@render themeIcon()}</Button
>
{#if data.user}
<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}
<Button href={resolve('login')} variant="outline" onclick={() => (menuOpen = false)}
>sign in</Button
>
<Button
href={resolve('register')}
variant="outline"
onclick={() => (menuOpen = false)}>register</Button
>
{/if}
</Sheet.Footer>
</Sheet.Content>
</Sheet.Root>
</div>
</header>
{/if}
<main id="main" class="isolate min-w-0 flex-1">{@render children()}</main>
{#if !immersive}
<footer
class="mt-16 border-t border-[var(--site-border)]"
class:tracker-footer={page.route.id === '/projects/game-tracker'}
class:cod-footer={page.route.id === '/projects/game-tracker/call-of-duty/[slug]'}
>
<div class="mx-auto max-w-6xl space-y-5 px-4 py-8 text-[var(--site-muted)] sm:px-6">
<div class="flex flex-wrap items-center justify-between gap-3 text-base">
<span
>stan runge <span class="ml-2 font-mono text-xs" title="Deployed commit"
>{data.version.slice(0, 7)}</span
></span
>
<a
href="https://git.stanrunge.dev/stan/website"
rel="external"
class="inline-flex items-center gap-1 hover:text-[var(--site-text)]"
>source <ExternalLink size={14} aria-hidden="true" /></a
>
</div>
<nav class="flex flex-wrap items-center justify-center gap-1" aria-label="Social links">
{#each socialLinks as { icon: Icon, label, href } (label)}
<Button
{href}
variant="ghost"
size="icon-lg"
rel="external"
aria-label={label}
title={label}><Icon class="size-6" /></Button
>
{/each}
{#each copyLinks as item (item.label)}<CopyButton {...item} />{/each}
</nav>
</div>
</footer>
{/if}
</div>
<style>