start: show the normal site header on /start (with the owner dashboard button, highlighted there); no footer on the new-tab page
All checks were successful
Deploy production / deploy (push) Successful in 2m36s
All checks were successful
Deploy production / deploy (push) Successful in 2m36s
/start is no longer immersive, so it loads the lazily imported SiteChrome like other pages. Only the book reader still skips the chrome. hidesFooter() keeps the footer off /start.
This commit is contained in:
@@ -1,13 +1,19 @@
|
|||||||
import { describe, expect, it } from 'vitest';
|
import { describe, expect, it } from 'vitest';
|
||||||
import { isImmersive } from './chrome';
|
import { hidesFooter, isImmersive } from './chrome';
|
||||||
|
|
||||||
describe('immersive pages', () => {
|
describe('immersive pages', () => {
|
||||||
it('skips the site chrome on /start and the book reader only', () => {
|
it('skips the site chrome on the book reader only', () => {
|
||||||
expect(isImmersive('/start')).toBe(true);
|
expect(isImmersive('/start')).toBe(false);
|
||||||
expect(isImmersive('/start/')).toBe(true);
|
|
||||||
expect(isImmersive('/trackers/books/12/read')).toBe(true);
|
expect(isImmersive('/trackers/books/12/read')).toBe(true);
|
||||||
expect(isImmersive('/')).toBe(false);
|
expect(isImmersive('/')).toBe(false);
|
||||||
expect(isImmersive('/starter')).toBe(false);
|
expect(isImmersive('/starter')).toBe(false);
|
||||||
expect(isImmersive('/trackers/books/12')).toBe(false);
|
expect(isImmersive('/trackers/books/12')).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('shows the header but no footer on /start', () => {
|
||||||
|
expect(hidesFooter('/start')).toBe(true);
|
||||||
|
expect(hidesFooter('/start/')).toBe(true);
|
||||||
|
expect(hidesFooter('/starter')).toBe(false);
|
||||||
|
expect(hidesFooter('/')).toBe(false);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
/**
|
/**
|
||||||
* Full-screen pages that render without the site header and footer: the /start new-tab
|
* Full-screen pages that render without the site header and footer (the book reader). They
|
||||||
* dashboard and the book reader. They also skip loading the header's JS (icons, menus,
|
* also skip loading the header's JS (icons, menus, the mobile Sheet): src/routes/+layout.ts
|
||||||
* the mobile Sheet): src/routes/+layout.ts only imports SiteChrome for other pages.
|
* only imports SiteChrome for other pages.
|
||||||
*/
|
*/
|
||||||
export function isImmersive(pathname: string): boolean {
|
export function isImmersive(pathname: string): boolean {
|
||||||
return /^\/start\/?$/.test(pathname) || /^\/trackers\/books\/\d+\/read\/?$/.test(pathname);
|
return /^\/trackers\/books\/\d+\/read\/?$/.test(pathname);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Pages that show the site header but no footer: the /start new-tab dashboard. */
|
||||||
|
export function hidesFooter(pathname: string): boolean {
|
||||||
|
return /^\/start\/?$/.test(pathname);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** True when a keyboard shortcut should not fire because the user is typing. */
|
/** True when a keyboard shortcut should not fire because the user is typing. */
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { onMount, type Snippet } from 'svelte';
|
import { onMount, type Snippet } from 'svelte';
|
||||||
import { resolve } from '$app/paths';
|
import { resolve } from '$app/paths';
|
||||||
import logo from '#lib/assets/brand/favicon-32x32.png?url';
|
import logo from '#lib/assets/brand/favicon-32x32.png?url';
|
||||||
import { isTyping } from '#lib/chrome.js';
|
import { hidesFooter, isTyping } from '#lib/chrome.js';
|
||||||
import { page } from '$app/state';
|
import { page } from '$app/state';
|
||||||
import CopyButton from '#lib/components/CopyButton.svelte';
|
import CopyButton from '#lib/components/CopyButton.svelte';
|
||||||
import ProjectIcon from '#lib/components/ProjectIcon.svelte';
|
import ProjectIcon from '#lib/components/ProjectIcon.svelte';
|
||||||
@@ -36,6 +36,7 @@
|
|||||||
version: string;
|
version: string;
|
||||||
};
|
};
|
||||||
let { data, children }: { data: ChromeData; children: Snippet } = $props();
|
let { data, children }: { data: ChromeData; children: Snippet } = $props();
|
||||||
|
const showFooter = $derived(!hidesFooter(page.url.pathname));
|
||||||
let menuOpen = $state(false);
|
let menuOpen = $state(false);
|
||||||
// Which header dropdown is open (desktop): '/projects', '/trackers' or none.
|
// Which header dropdown is open (desktop): '/projects', '/trackers' or none.
|
||||||
let openMenu = $state<string | null>(null);
|
let openMenu = $state<string | null>(null);
|
||||||
@@ -266,7 +267,7 @@
|
|||||||
{#if data.isOwner}
|
{#if data.isOwner}
|
||||||
<Button
|
<Button
|
||||||
href={resolve('/start')}
|
href={resolve('/start')}
|
||||||
variant="ghost"
|
variant={current('/start') ? 'secondary' : 'ghost'}
|
||||||
size="sm"
|
size="sm"
|
||||||
class="gap-1.5"
|
class="gap-1.5"
|
||||||
title="Dashboard (g then d)"
|
title="Dashboard (g then d)"
|
||||||
@@ -369,40 +370,42 @@
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<main id="main" class="isolate min-w-0 flex-1">{@render children()}</main>
|
<main id="main" class="isolate min-w-0 flex-1">{@render children()}</main>
|
||||||
<footer
|
{#if showFooter}
|
||||||
class="mt-16 border-t border-[var(--site-border)]"
|
<footer
|
||||||
class:tracker-footer={page.route.id === '/projects/game-tracker'}
|
class="mt-16 border-t border-[var(--site-border)]"
|
||||||
class:cod-footer={page.route.id === '/projects/game-tracker/call-of-duty/[slug]'}
|
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">
|
<div class="mx-auto max-w-6xl space-y-5 px-4 py-8 text-[var(--site-muted)] sm:px-6">
|
||||||
<span
|
<div class="flex flex-wrap items-center justify-between gap-3 text-base">
|
||||||
>stan runge <span class="ml-2 font-mono text-xs" title="Deployed commit"
|
<span
|
||||||
>{data.version.slice(0, 7)}</span
|
>stan runge <span class="ml-2 font-mono text-xs" title="Deployed commit"
|
||||||
></span
|
>{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}
|
<a
|
||||||
{#each copyLinks as item (item.label)}<CopyButton {...item} />{/each}
|
href="https://git.stanrunge.dev/stan/website"
|
||||||
</nav>
|
rel="external"
|
||||||
</div>
|
class="inline-flex items-center gap-1 hover:text-[var(--site-text)]"
|
||||||
</footer>
|
>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>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
import { getSeo } from '#lib/seo.js';
|
import { getSeo } from '#lib/seo.js';
|
||||||
|
|
||||||
// The header/footer are in #lib/components/SiteChrome.svelte, which +layout.ts loads only
|
// The header/footer are in #lib/components/SiteChrome.svelte, which +layout.ts loads only
|
||||||
// for pages that show them, so the /start new-tab page stays small.
|
// for pages that show them (not the full-screen book reader).
|
||||||
let { data, children } = $props();
|
let { data, children } = $props();
|
||||||
const seo = $derived(getSeo(page.url.pathname, page.data, data.environment));
|
const seo = $derived(getSeo(page.url.pathname, page.data, data.environment));
|
||||||
const Chrome = $derived(data.Chrome);
|
const Chrome = $derived(data.Chrome);
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import { isImmersive } from '#lib/chrome.js';
|
|||||||
import type { LayoutLoad } from './$types';
|
import type { LayoutLoad } from './$types';
|
||||||
|
|
||||||
// The site header/footer (icon libraries, dropdowns, the bits-ui Sheet) live in SiteChrome,
|
// The site header/footer (icon libraries, dropdowns, the bits-ui Sheet) live in SiteChrome,
|
||||||
// imported only for pages that show them. Immersive pages such as the /start new-tab
|
// imported only for pages that show them. Immersive pages (the book reader) never
|
||||||
// dashboard never download that code. Universal loads may return components.
|
// download that code. Universal loads may return components.
|
||||||
export const load: LayoutLoad = async ({ data, url }) => ({
|
export const load: LayoutLoad = async ({ data, url }) => ({
|
||||||
...data,
|
...data,
|
||||||
Chrome: isImmersive(url.pathname)
|
Chrome: isImmersive(url.pathname)
|
||||||
|
|||||||
Reference in New Issue
Block a user