Revert to static JS site (Svelte built, but no CSR); Optimize images
This commit is contained in:
60
src/app.css
60
src/app.css
@@ -44,6 +44,66 @@ body {
|
||||
color: var(--fg);
|
||||
}
|
||||
|
||||
/* Lightbox (structure may be created by script.js; ensures styles apply to injected content) */
|
||||
.lightbox {
|
||||
align-items: stretch;
|
||||
background: var(--surface-elevated);
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
box-shadow: var(--lightbox-shadow);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
justify-content: center;
|
||||
max-width: 90vw;
|
||||
padding: 0.5rem 1rem;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
|
||||
.lightbox::backdrop {
|
||||
background: var(--lightbox-backdrop);
|
||||
}
|
||||
|
||||
.lightbox[open] {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.lightbox header {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding: 0.25rem 0.25rem 0 0;
|
||||
}
|
||||
|
||||
.lightbox .lb-close {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 2rem;
|
||||
color: var(--fg);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.lightbox .lb-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.lightbox .lb-content img,
|
||||
.lightbox .lb-content video {
|
||||
max-width: 90vw;
|
||||
max-height: 80vh;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.lightbox .lb-caption {
|
||||
color: var(--fg);
|
||||
margin-top: 0.5rem;
|
||||
text-align: center;
|
||||
max-width: 90vw;
|
||||
}
|
||||
|
||||
.lightbox-open {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@@ -4,25 +4,18 @@
|
||||
interface Props {
|
||||
index?: number;
|
||||
item: MediaItem;
|
||||
showLightbox: (item: MediaItem) => void;
|
||||
}
|
||||
|
||||
let { item, index, showLightbox }: Props = $props();
|
||||
let { item, index }: Props = $props();
|
||||
</script>
|
||||
|
||||
<button
|
||||
class={`gallery-item ${item.type === 'video' ? ' video' : ''}`}
|
||||
type="button"
|
||||
tabindex="0"
|
||||
data-name={item.name}
|
||||
data-type={item.type}
|
||||
data-caption={item.caption}
|
||||
onclick={() => showLightbox(item)}
|
||||
onkeydown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
showLightbox(item);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<figure>
|
||||
<picture>
|
||||
|
||||
@@ -1,77 +1,20 @@
|
||||
<script lang="ts">
|
||||
import type { MediaItem } from '$lib/media';
|
||||
|
||||
interface Props {
|
||||
item: MediaItem | null;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
let { item, onClose }: Props = $props();
|
||||
|
||||
let ref = $state<HTMLDialogElement | null>(null);
|
||||
|
||||
$effect(() => {
|
||||
if (ref && item) {
|
||||
document.body.style.overflow = 'hidden';
|
||||
ref.showModal();
|
||||
} else {
|
||||
document.body.style.overflow = 'auto';
|
||||
ref?.close();
|
||||
}
|
||||
});
|
||||
/**
|
||||
* Structure-only lightbox shell. Content is injected by static/assets/js/script.js
|
||||
* (no client-side Svelte; csr = false). Keeps HTML/CSS in one place.
|
||||
*/
|
||||
</script>
|
||||
|
||||
<dialog
|
||||
class="lightbox"
|
||||
aria-hidden="true"
|
||||
onclose={onClose}
|
||||
closedby="any"
|
||||
aria-describedby="lb-caption"
|
||||
bind:this={ref}
|
||||
>
|
||||
{#if item}
|
||||
<header>
|
||||
<button
|
||||
class="lb-close"
|
||||
aria-label="Close"
|
||||
onclick={() => ref?.close()}
|
||||
onkeydown={(e) => {
|
||||
if (e.key === 'Enter') ref?.close();
|
||||
}}>×</button
|
||||
>
|
||||
</header>
|
||||
<div class="lb-content">
|
||||
{#if item?.type === 'video'}
|
||||
<video
|
||||
src={`/assets/media/videos/${item?.name}.mp4`}
|
||||
controls
|
||||
autoplay
|
||||
>
|
||||
<track
|
||||
kind="captions"
|
||||
src={`/assets/media/videos/${item?.name}-captions.vtt`}
|
||||
default
|
||||
/>
|
||||
</video>
|
||||
{:else}
|
||||
<picture>
|
||||
{#each [{ bp: 'desktop', minWidth: 1024 }, { bp: 'tablet', minWidth: 768 }, { bp: 'mobile', minWidth: 0 }] as breakpoint}
|
||||
<source
|
||||
media="(min-width:{breakpoint.minWidth}px)"
|
||||
srcset={item?.type === 'image'
|
||||
? `/assets/media/${breakpoint.bp}/${item?.name}@1x.webp 1x, /assets/media/${breakpoint.bp}/${item?.name}.webp 2x`
|
||||
: `/assets/media/${breakpoint.bp}/${item?.name}_still@1x.webp 1x, /assets/media/${breakpoint.bp}/${item?.name}_still.webp 2x`}
|
||||
/>
|
||||
{/each}
|
||||
<img
|
||||
src="/assets/media/thumbnail/{item?.name}.webp"
|
||||
alt={item?.alt.replace(/['']/g, '')}
|
||||
/>
|
||||
</picture>
|
||||
{/if}
|
||||
</div>
|
||||
<p id="lb-caption" class="lb-caption">{item?.caption}</p>
|
||||
{/if}
|
||||
<dialog class="lightbox" aria-hidden="true" aria-describedby="lb-caption" closedby="any">
|
||||
<header>
|
||||
<button type="button" class="lb-close" aria-label="Close"
|
||||
>×</button
|
||||
>
|
||||
</header>
|
||||
<div class="lb-content">
|
||||
<!-- script.js injects image or video here -->
|
||||
</div>
|
||||
<p id="lb-caption" class="lb-caption"></p>
|
||||
</dialog>
|
||||
|
||||
<style>
|
||||
@@ -98,8 +41,9 @@
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
& img,
|
||||
& video {
|
||||
/* Injected by script.js */
|
||||
:global(img),
|
||||
:global(video) {
|
||||
max-width: 90vw;
|
||||
max-height: 80vh;
|
||||
border-radius: 8px;
|
||||
@@ -117,6 +61,7 @@
|
||||
border: none;
|
||||
font-size: 2rem;
|
||||
color: var(--fg);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.lb-content {
|
||||
@@ -125,7 +70,8 @@
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
& img {
|
||||
/* Injected by script.js */
|
||||
:global(img) {
|
||||
max-width: 90vw;
|
||||
max-height: 80vh;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<script lang="ts">
|
||||
import { theme } from '$lib/stores/theme.svelte.js';
|
||||
|
||||
const toggleTheme = () => {
|
||||
theme.set(theme.get() === 'light' ? 'dark' : 'light');
|
||||
};
|
||||
interface Props {
|
||||
/** Caption for the tour video (used by script.js for lightbox); from media data */
|
||||
tourCaption?: string;
|
||||
}
|
||||
let { tourCaption }: Props = $props();
|
||||
</script>
|
||||
|
||||
<header class="site-header">
|
||||
@@ -13,6 +13,9 @@
|
||||
id="show_video"
|
||||
class="emoji-button"
|
||||
aria-label="Show video tour"
|
||||
data-name="tour"
|
||||
data-type="video"
|
||||
data-caption={tourCaption}
|
||||
>
|
||||
🎥
|
||||
</button>
|
||||
@@ -20,7 +23,6 @@
|
||||
id="theme-toggle"
|
||||
class="emoji-button"
|
||||
aria-label="Toggle light/dark theme"
|
||||
onclick={toggleTheme}
|
||||
>
|
||||
🌓
|
||||
</button>
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
export const prerender = true;
|
||||
export const ssr = true;
|
||||
export const csr = false;
|
||||
|
||||
@@ -29,16 +29,6 @@
|
||||
addressCountry: 'US',
|
||||
},
|
||||
};
|
||||
|
||||
let showPicture = $state<MediaItem | null>(null);
|
||||
|
||||
const showLightbox = (item: MediaItem) => {
|
||||
showPicture = item;
|
||||
};
|
||||
|
||||
const onClose = () => {
|
||||
showPicture = null;
|
||||
};
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
@@ -77,12 +67,14 @@
|
||||
{@html `<script type="application/ld+json">${JSON.stringify(jsonLd)}</script>`}
|
||||
</svelte:head>
|
||||
|
||||
<SiteHeader />
|
||||
<SiteHeader
|
||||
tourCaption={data.mediaItems.find((m) => m.name === 'tour')?.caption}
|
||||
/>
|
||||
<main>
|
||||
<section id="gallery" class="gallery-grid">
|
||||
{#each data.mediaItems as item, index (item.name)}
|
||||
<GalleryFigure {item} {index} {showLightbox} />
|
||||
<GalleryFigure {item} {index} />
|
||||
{/each}
|
||||
</section>
|
||||
</main>
|
||||
<Lightbox item={showPicture} {onClose} />
|
||||
<Lightbox />
|
||||
|
||||
Reference in New Issue
Block a user