From 4f863e568643a1cefa2517842a92212e5f1ab59d Mon Sep 17 00:00:00 2001 From: mifi Date: Sun, 15 Feb 2026 23:01:16 -0300 Subject: [PATCH] Finished the Sveltification --- package.json | 3 +- pnpm-lock.yaml | 15 +- scripts/externalize-bootstrap.js | 79 ++++ src/app.css | 118 +++--- src/app.d.ts | 14 +- src/lib/components/GalleryFigure.svelte | 161 ++++---- src/lib/components/Lightbox.svelte | 140 +++++++ src/lib/components/SiteHeader.svelte | 65 ++-- src/lib/media.ts | 350 +++++++++--------- src/lib/stores/theme.svelte.ts | 10 + src/routes/+layout.svelte | 6 +- src/routes/+layout.ts | 1 + src/routes/+page.svelte | 124 ++++--- src/routes/+page.ts | 2 +- .../appspecific/com.chrome.devtools.json | 1 + static/assets/js/script.js | 107 +----- static/assets/media/videos/tour-captions.vtt | 4 + 17 files changed, 683 insertions(+), 517 deletions(-) create mode 100644 scripts/externalize-bootstrap.js create mode 100644 src/lib/components/Lightbox.svelte create mode 100644 src/lib/stores/theme.svelte.ts create mode 100644 static/.well-known/appspecific/com.chrome.devtools.json create mode 100644 static/assets/media/videos/tour-captions.vtt diff --git a/package.json b/package.json index 416ff28..b20fca3 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "description": "Armandine gallery – pre-rendered Svelte site", "scripts": { "dev": "vite dev", - "build": "vite build && node scripts/critical-css.js", + "build": "vite build && node scripts/critical-css.js && node scripts/externalize-bootstrap.js", "preview": "vite preview", "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", @@ -42,6 +42,7 @@ "stylelint": "^17.3.0", "stylelint-config-standard": "^40.0.0", "svelte": "^5.0.0", + "terser": "^5.0.0", "svelte-check": "^4.0.0", "typescript": "^5.0.0", "vite": "^7.3.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c5262d6..d85fd10 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -59,6 +59,9 @@ importers: svelte-check: specifier: ^4.0.0 version: 4.4.0(picomatch@4.0.3)(svelte@5.51.2)(typescript@5.9.3) + terser: + specifier: ^5.0.0 + version: 5.46.0 typescript: specifier: ^5.0.0 version: 5.9.3 @@ -2571,7 +2574,6 @@ snapshots: dependencies: '@jridgewell/gen-mapping': 0.3.13 '@jridgewell/trace-mapping': 0.3.31 - optional: true '@jridgewell/sourcemap-codec@1.5.5': {} @@ -2921,8 +2923,7 @@ snapshots: node-releases: 2.0.27 update-browserslist-db: 1.2.3(browserslist@4.28.1) - buffer-from@1.1.2: - optional: true + buffer-from@1.1.2: {} cacheable@2.3.2: dependencies: @@ -2956,8 +2957,7 @@ snapshots: colord@2.9.3: {} - commander@2.20.3: - optional: true + commander@2.20.3: {} consola@2.15.3: {} @@ -3820,10 +3820,8 @@ snapshots: dependencies: buffer-from: 1.1.2 source-map: 0.6.1 - optional: true - source-map@0.6.1: - optional: true + source-map@0.6.1: {} string-width@4.2.3: dependencies: @@ -3952,7 +3950,6 @@ snapshots: acorn: 8.15.0 commander: 2.20.3 source-map-support: 0.5.21 - optional: true tinyglobby@0.2.15: dependencies: diff --git a/scripts/externalize-bootstrap.js b/scripts/externalize-bootstrap.js new file mode 100644 index 0000000..09cdbe5 --- /dev/null +++ b/scripts/externalize-bootstrap.js @@ -0,0 +1,79 @@ +/** + * Move SvelteKit's inline bootstrap script to an external file for CSP (no unsafe-inline). + * Run after vite build; reads/writes build/. + * Finds containing __sveltekit_, minifies it, writes to _app/immutable/entry/bootstrap.js, + * and replaces the inline script with that contains __sveltekit_ +function findInlineBootstrap(html) { + const scriptOpen = html.indexOf('', scriptOpen); + if (scriptClose === -1) return null; + const content = html.slice(scriptOpen + ''.length }; +} + +const SCRIPT_TAG = ``; + +async function main() { + const htmlFiles = getFiles(buildDir, '.html'); + let bootstrapWritten = false; + let count = 0; + for (const htmlFile of htmlFiles) { + let html = readFileSync(htmlFile, 'utf8'); + const found = findInlineBootstrap(html); + if (!found) continue; + + if (!bootstrapWritten) { + // Imports relative to script location when in _app/immutable/entry/ + let scriptContent = found.content.replace( + /import\("\.\/_app\/immutable\/entry\/([^"]+)"\)/g, + 'import("./$1")' + ); + const result = await minify(scriptContent, { + format: { comments: false }, + compress: { passes: 1 } + }); + if (result.code) scriptContent = result.code; + mkdirSync(entryDir, { recursive: true }); + writeFileSync(bootstrapPath, scriptContent, 'utf8'); + bootstrapWritten = true; + } + + html = html.slice(0, found.start) + SCRIPT_TAG + html.slice(found.end); + writeFileSync(htmlFile, html, 'utf8'); + count++; + } + if (count > 0) { + console.log('Bootstrap script externalized (minified):', scriptSrc, `(${count} HTML file(s))`); + } else if (htmlFiles.length > 0) { + console.log('No SvelteKit inline script found in HTML (bootstrap already external?)'); + } +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/src/app.css b/src/app.css index 363e732..494a5e3 100644 --- a/src/app.css +++ b/src/app.css @@ -1,99 +1,67 @@ :root { - --bg: #fff; - --fg: #222; - --accent: #007acc; + --bg: #fff; + --fg: #222; + --accent: #007acc; + --lightbox-backdrop: rgb(255 255 255 / 90%); + --lightbox-shadow: 0 0 10px 0 rgb(0 0 0 / 10%); + --surface-elevated: #f3f3f3; } @media (prefers-color-scheme: dark) { - :root { - --bg: #111; - --fg: #eee; - --accent: #46c; - } + :root { + --bg: #111; + --fg: #eee; + --accent: #46c; + --lightbox-backdrop: rgb(0 0 0 / 90%); + --surface-elevated: #222; + } } /* Explicit theme toggle overrides (win over media query when set) */ -html.dark { - --bg: #111; - --fg: #eee; - --accent: #46c; -} +html { + &[data-theme='dark'] { + --bg: #111; + --fg: #eee; + --accent: #46c; + --lightbox-backdrop: rgb(0 0 0 / 90%); + --surface-elevated: #222; + } -html.light { - --bg: #fff; - --fg: #222; - --accent: #007acc; + &[data-theme='light'] { + --bg: #fff; + --fg: #222; + --accent: #007acc; + --lightbox-backdrop: rgb(255 255 255 / 90%); + --lightbox-shadow: 0 0 10px 0 rgb(0 0 0 / 10%); + --surface-elevated: #f3f3f3; + } } body { - margin: 0; - font-family: sans-serif; - background-color: var(--bg); - color: var(--fg); + margin: 0; + font-family: sans-serif; + background-color: var(--bg); + color: var(--fg); } .lightbox-open { - overflow: hidden; + overflow: hidden; } .gallery-grid { - column-count: 1; - column-gap: 1rem; - padding: 1rem; + column-count: 1; + column-gap: 1rem; + padding: 1rem; } @media (width >= 768px) { - .gallery-grid { - column-count: 2; - } + .gallery-grid { + column-count: 2; + } } @media (width >= 1024px) { - .gallery-grid { - column-count: 3; - } -} - -/* Lightbox: not a Svelte component, styled globally for script.js target */ -#lightbox { - position: fixed; - inset: 0; - background: rgb(0 0 0 / 90%); - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - visibility: hidden; - opacity: 0; - transition: opacity 0.3s; -} - -#lightbox[aria-hidden='false'] { - visibility: visible; - opacity: 1; -} - -#lb-content img, -#lb-content video { - max-width: 90vw; - max-height: 80vh; - border-radius: 8px; -} - -#lb-caption { - color: #fff; - margin-top: 0.5rem; - text-align: center; - max-width: 90vw; -} - -#lb-close { - position: absolute; - top: 1rem; - right: 1rem; - background: none; - border: none; - font-size: 2rem; - color: #fff; - cursor: pointer; + .gallery-grid { + column-count: 3; + } } diff --git a/src/app.d.ts b/src/app.d.ts index da08e6d..d76242a 100644 --- a/src/app.d.ts +++ b/src/app.d.ts @@ -1,13 +1,13 @@ // See https://svelte.dev/docs/kit/types#app.d.ts // for information about these interfaces declare global { - namespace App { - // interface Error {} - // interface Locals {} - // interface PageData {} - // interface PageState {} - // interface Platform {} - } + namespace App { + // interface Error {} + // interface Locals {} + // interface PageData {} + // interface PageState {} + // interface Platform {} + } } export {}; diff --git a/src/lib/components/GalleryFigure.svelte b/src/lib/components/GalleryFigure.svelte index 5383b29..34cd1db 100644 --- a/src/lib/components/GalleryFigure.svelte +++ b/src/lib/components/GalleryFigure.svelte @@ -1,83 +1,100 @@ - - +
+ + {#each [{ bp: 'desktop', minWidth: 1024 }, { bp: 'tablet', minWidth: 768 }, { bp: 'mobile', minWidth: 0 }] as breakpoint} + + {/each} + {item.alt.replace(/['']/g, 2 + ? (item.loading ?? 'lazy') + : undefined} + fetchpriority={item.fetchpriority ?? undefined} + /> + +
{item.caption}
+
+ diff --git a/src/lib/components/Lightbox.svelte b/src/lib/components/Lightbox.svelte new file mode 100644 index 0000000..f132351 --- /dev/null +++ b/src/lib/components/Lightbox.svelte @@ -0,0 +1,140 @@ + + + + + diff --git a/src/lib/components/SiteHeader.svelte b/src/lib/components/SiteHeader.svelte index 5afe330..3ef2a70 100644 --- a/src/lib/components/SiteHeader.svelte +++ b/src/lib/components/SiteHeader.svelte @@ -1,30 +1,47 @@ + + diff --git a/src/lib/media.ts b/src/lib/media.ts index 8636f9f..7d41b3d 100644 --- a/src/lib/media.ts +++ b/src/lib/media.ts @@ -1,178 +1,182 @@ export interface MediaItem { - type: 'image' | 'video'; - name: string; - caption: string; - alt: string; - height?: number; - width?: number; - loading?: 'lazy' | 'eager'; - fetchpriority?: 'high' | 'low' | 'auto'; + type: 'image' | 'video'; + name: string; + caption: string; + alt: string; + height?: number; + width?: number; + loading?: 'lazy' | 'eager'; + fetchpriority?: 'high' | 'low' | 'auto'; } export const mediaItems: MediaItem[] = [ - { - type: 'image', - name: 'living_room_1', - caption: 'An inviting blend of comfort and curated art—relaxation guaranteed.', - alt: 'Sunny living room with stylish seating and vibrant artwork.', - height: 200, - width: 300, - loading: 'eager', - fetchpriority: 'high' - }, - { - type: 'image', - name: 'living_room_2', - caption: 'Relaxation elevated—your stylish living space awaits.', - alt: 'Spacious living area featuring elegant furniture and tasteful decor.', - height: 200, - width: 300, - fetchpriority: 'high' - }, - { - type: 'image', - name: 'kitchen', - caption: 'The culinary stage is set—snacking encouraged, style required.', - alt: 'Modern kitchen showcasing sleek appliances and contemporary design.', - height: 200, - width: 300 - }, - { - type: 'image', - name: 'bedroom_suite_1', - caption: 'A bedroom suite designed to make snoozing irresistible.', - alt: 'Inviting bedroom suite with cozy bedding and warm lighting.', - height: 200, - width: 300 - }, - { - type: 'image', - name: 'bedroom_suite_2', - caption: 'Style meets comfort—sleeping in has never been easier.', - alt: 'Comfortable bedroom suite with elegant decor and soft tones.', - height: 200, - width: 300 - }, - { - type: 'image', - name: 'bedroom_suite_3', - caption: 'Where dreams get stylish—a bedroom that feels like home.', - alt: 'Welcoming bedroom with soothing colors and inviting ambiance.', - height: 200, - width: 300 - }, - { - type: 'image', - name: 'guest_bath', - caption: 'Your personal spa experience—right down the hall.', - alt: 'Sophisticated guest bathroom with modern fixtures and clean lines.', - height: 450, - width: 300 - }, - { - type: 'image', - name: 'onsuite_1', - caption: 'Luxury meets practicality—your private ensuite awaits.', - alt: 'Private ensuite bathroom featuring contemporary design and premium finishes.', - height: 450, - width: 300, - loading: 'eager', - fetchpriority: 'high' - }, - { - type: 'image', - name: 'onsuite_2', - caption: 'Everyday luxury, right at home—your ensuite oasis.', - alt: 'Elegant ensuite with sleek fixtures and stylish decor.', - height: 200, - width: 300 - }, - { - type: 'image', - name: 'laundry', - caption: 'Laundry day reimagined—functional never looked so good.', - alt: 'Modern laundry room with washer, dryer, and organized storage.', - height: 450, - width: 300 - }, - { - type: 'image', - name: 'coat_closet', - caption: 'Organized and chic—your entryway\'s best friend.', - alt: 'Convenient coat closet with tidy storage solutions.', - height: 200, - width: 300 - }, - { - type: 'image', - name: 'deck_1', - caption: 'Outdoor comfort, just steps away—morning coffee optional.', - alt: 'Sunny deck with cozy seating and pleasant outdoor views.', - height: 450, - width: 300 - }, - { - type: 'image', - name: 'deck_2', - caption: 'Your fresh-air escape—ideal for relaxing evenings.', - alt: 'Comfortable deck area perfect for unwinding or entertaining.', - height: 200, - width: 300 - }, - { - type: 'image', - name: 'exterior', - caption: 'Curb appeal perfected—your new favorite place starts here.', - alt: 'Attractive home exterior with inviting architecture.', - height: 200, - width: 300 - }, - { - type: 'image', - name: 'backyard_parking', - caption: 'Convenience meets privacy—your personal backyard parking spot.', - alt: 'Private backyard parking area offering secure convenience.', - height: 200, - width: 300 - }, - { - type: 'image', - name: 'office_fitness_guest_1', - caption: 'Productivity zone meets fitness corner—multitasking done right.', - alt: 'Dual-purpose room featuring office setup and fitness equipment.', - height: 200, - width: 300 - }, - { - type: 'image', - name: 'office_fitness_guest_2', - caption: 'Work, workout, or unwind—the room of endless possibilities.', - alt: 'Versatile office and fitness area with modern amenities.', - height: 200, - width: 300 - }, - { - type: 'image', - name: 'office_fitness_guest_3', - caption: 'Stay focused or get fit—you decide.', - alt: 'Functional space combining a workspace and home fitness area.', - height: 200, - width: 300 - }, - { - type: 'image', - name: 'office_fitness_guest_4', - caption: 'Room for every routine—your workspace meets wellness.', - alt: 'Stylish office area seamlessly integrated with fitness features.', - height: 200, - width: 300 - }, - { - type: 'video', - name: 'tour', - caption: "Take the scenic route—explore your the home's highlights with a virtual walkthrough.", - alt: 'Video tour showcasing the property.', - height: 534, - width: 300 - } + { + type: 'image', + name: 'living_room_1', + caption: + 'An inviting blend of comfort and curated art—relaxation guaranteed.', + alt: 'Sunny living room with stylish seating and vibrant artwork.', + height: 200, + width: 300, + fetchpriority: 'high', + }, + { + type: 'image', + name: 'living_room_2', + caption: 'Relaxation elevated—your stylish living space awaits.', + alt: 'Spacious living area featuring elegant furniture and tasteful decor.', + height: 200, + width: 300, + fetchpriority: 'high', + }, + { + type: 'image', + name: 'kitchen', + caption: + 'The culinary stage is set—snacking encouraged, style required.', + alt: 'Modern kitchen showcasing sleek appliances and contemporary design.', + height: 200, + width: 300, + }, + { + type: 'image', + name: 'bedroom_suite_1', + caption: 'A bedroom suite designed to make snoozing irresistible.', + alt: 'Inviting bedroom suite with cozy bedding and warm lighting.', + height: 200, + width: 300, + }, + { + type: 'image', + name: 'bedroom_suite_2', + caption: 'Style meets comfort—sleeping in has never been easier.', + alt: 'Comfortable bedroom suite with elegant decor and soft tones.', + height: 200, + width: 300, + }, + { + type: 'image', + name: 'bedroom_suite_3', + caption: 'Where dreams get stylish—a bedroom that feels like home.', + alt: 'Welcoming bedroom with soothing colors and inviting ambiance.', + height: 200, + width: 300, + }, + { + type: 'image', + name: 'guest_bath', + caption: 'Your personal spa experience—right down the hall.', + alt: 'Sophisticated guest bathroom with modern fixtures and clean lines.', + height: 450, + width: 300, + }, + { + type: 'image', + name: 'onsuite_1', + caption: 'Luxury meets practicality—your private ensuite awaits.', + alt: 'Private ensuite bathroom featuring contemporary design and premium finishes.', + height: 450, + width: 300, + loading: 'eager', + fetchpriority: 'high', + }, + { + type: 'image', + name: 'onsuite_2', + caption: 'Everyday luxury, right at home—your ensuite oasis.', + alt: 'Elegant ensuite with sleek fixtures and stylish decor.', + height: 200, + width: 300, + }, + { + type: 'image', + name: 'laundry', + caption: 'Laundry day reimagined—functional never looked so good.', + alt: 'Modern laundry room with washer, dryer, and organized storage.', + height: 450, + width: 300, + }, + { + type: 'image', + name: 'coat_closet', + caption: "Organized and chic—your entryway's best friend.", + alt: 'Convenient coat closet with tidy storage solutions.', + height: 200, + width: 300, + }, + { + type: 'image', + name: 'deck_1', + caption: 'Outdoor comfort, just steps away—morning coffee optional.', + alt: 'Sunny deck with cozy seating and pleasant outdoor views.', + height: 450, + width: 300, + }, + { + type: 'image', + name: 'deck_2', + caption: 'Your fresh-air escape—ideal for relaxing evenings.', + alt: 'Comfortable deck area perfect for unwinding or entertaining.', + height: 200, + width: 300, + }, + { + type: 'image', + name: 'exterior', + caption: 'Curb appeal perfected—your new favorite place starts here.', + alt: 'Attractive home exterior with inviting architecture.', + height: 200, + width: 300, + }, + { + type: 'image', + name: 'backyard_parking', + caption: + 'Convenience meets privacy—your personal backyard parking spot.', + alt: 'Private backyard parking area offering secure convenience.', + height: 200, + width: 300, + }, + { + type: 'image', + name: 'office_fitness_guest_1', + caption: + 'Productivity zone meets fitness corner—multitasking done right.', + alt: 'Dual-purpose room featuring office setup and fitness equipment.', + height: 200, + width: 300, + }, + { + type: 'image', + name: 'office_fitness_guest_2', + caption: 'Work, workout, or unwind—the room of endless possibilities.', + alt: 'Versatile office and fitness area with modern amenities.', + height: 200, + width: 300, + }, + { + type: 'image', + name: 'office_fitness_guest_3', + caption: 'Stay focused or get fit—you decide.', + alt: 'Functional space combining a workspace and home fitness area.', + height: 200, + width: 300, + }, + { + type: 'image', + name: 'office_fitness_guest_4', + caption: 'Room for every routine—your workspace meets wellness.', + alt: 'Stylish office area seamlessly integrated with fitness features.', + height: 200, + width: 300, + }, + { + type: 'video', + name: 'tour', + caption: + "Take the scenic route—explore your the home's highlights with a virtual walkthrough.", + alt: 'Video tour showcasing the property.', + height: 534, + width: 300, + }, ]; diff --git a/src/lib/stores/theme.svelte.ts b/src/lib/stores/theme.svelte.ts new file mode 100644 index 0000000..3de7dfc --- /dev/null +++ b/src/lib/stores/theme.svelte.ts @@ -0,0 +1,10 @@ +let mode = $state<'light' | 'dark'>('light'); + +export const theme = { + get: () => mode, + set: (value: 'light' | 'dark') => { + mode = value; + document.documentElement.setAttribute('data-theme', value); + localStorage.setItem('theme', value); + }, +}; diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index f87b35e..8921562 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -1,5 +1,7 @@ - +{@render children()} diff --git a/src/routes/+layout.ts b/src/routes/+layout.ts index 189f71e..0df44ad 100644 --- a/src/routes/+layout.ts +++ b/src/routes/+layout.ts @@ -1 +1,2 @@ export const prerender = true; +export const ssr = true; diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 3ac0ada..2a7967e 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -1,68 +1,88 @@ - {title} - - - - - - - + {title} + + + + + + + - - - + + + - - + + - - + + - {@html ``} + {@html ``}
- +
- - - + diff --git a/src/routes/+page.ts b/src/routes/+page.ts index e2717c6..074d678 100644 --- a/src/routes/+page.ts +++ b/src/routes/+page.ts @@ -1,5 +1,5 @@ import { mediaItems } from '$lib/media.js'; export function load() { - return { mediaItems }; + return { mediaItems }; } diff --git a/static/.well-known/appspecific/com.chrome.devtools.json b/static/.well-known/appspecific/com.chrome.devtools.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/static/.well-known/appspecific/com.chrome.devtools.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/static/assets/js/script.js b/static/assets/js/script.js index 8924b60..fac19d1 100644 --- a/static/assets/js/script.js +++ b/static/assets/js/script.js @@ -1,108 +1,13 @@ -// --- theme toggle --- -const toggle = document.getElementById('theme-toggle'); const root = document.documentElement; -const saved = window?.localStorage?.getItem('dark-mode'); +const saved = window?.localStorage?.getItem('theme'); const sysDark = window.matchMedia('(prefers-color-scheme: dark)').matches; -if (saved === 'true') { - root.classList.add('dark'); - root.classList.remove('light'); -} else if (saved === 'false') { - root.classList.add('light'); - root.classList.remove('dark'); + +if (saved) { + root.setAttribute('data-theme', saved); } else { if (sysDark) { - root.classList.add('dark'); - root.classList.remove('light'); + root.setAttribute('data-theme', 'dark'); } else { - root.classList.add('light'); - root.classList.remove('dark'); + root.setAttribute('data-theme', 'light'); } } -toggle?.addEventListener('click', () => { - const isDark = root.classList.contains('dark'); - root.classList.toggle('dark', !isDark); - root.classList.toggle('light', isDark); - window?.localStorage?.setItem('dark-mode', String(!isDark)); -}); - -const { body } = document; - -// --- lightbox base --- -const lb = document.getElementById('lightbox'); -const lbCnt = document.getElementById('lb-content'); -const lbCap = document.getElementById('lb-caption'); -document.getElementById('lb-close')?.addEventListener('click', () => { - lb?.setAttribute('aria-hidden', 'true'); - body.classList.remove('lightbox-open'); - if (lbCnt) lbCnt.innerHTML = ''; -}); - -// Build picture element for lightbox (name, type only) -function createPicture(name, type) { - const pic = document.createElement('picture'); - const breakpoints = [ - { bp: 'desktop', minWidth: 1024 }, - { bp: 'tablet', minWidth: 768 }, - { bp: 'mobile', minWidth: 0 } - ]; - for (const { bp, minWidth } of breakpoints) { - const src = document.createElement('source'); - src.media = `(min-width:${minWidth}px)`; - if (type === 'image') { - src.srcset = - `/assets/media/${bp}/${name}@1x.webp 1x, /assets/media/${bp}/${name}.webp 2x`; - } else { - src.srcset = - `/assets/media/${bp}/${name}_still@1x.webp 1x, /assets/media/${bp}/${name}_still.webp 2x`; - } - pic.appendChild(src); - } - const img = document.createElement('img'); - img.src = `/assets/media/thumbnail/${name}.webp`; - img.alt = ''; - pic.appendChild(img); - return pic; -} - -function openLightbox(item) { - if (!lbCnt || !lbCap || !lb) return; - lbCnt.innerHTML = ''; - if (item.type === 'video') { - const v = document.createElement('video'); - v.src = `/assets/media/videos/${item.name}.mp4`; - v.controls = true; - v.autoplay = true; - lbCnt.appendChild(v); - } else { - lbCnt.appendChild(createPicture(item.name, item.type)); - } - lbCap.textContent = item.caption; - body.classList.add('lightbox-open'); - lb.setAttribute('aria-hidden', 'false'); -} - -// --- bind to pre-rendered gallery --- -document.querySelectorAll('.gallery-item').forEach((fig) => { - const name = fig.getAttribute('data-name'); - const type = fig.getAttribute('data-type'); - const caption = fig.getAttribute('data-caption'); - if (!name || !type || !caption) return; - const item = { name, type, caption }; - fig.addEventListener('click', () => openLightbox(item)); - fig.addEventListener('keydown', (e) => { - if (e.key === 'Enter') openLightbox(item); - }); -}); - -// --- video toggle --- -const videoTgl = document.getElementById('show_video'); -videoTgl?.addEventListener('click', () => { - const videoFig = document.querySelector('.gallery-item.video'); - if (videoFig) { - openLightbox({ - name: videoFig.getAttribute('data-name'), - type: videoFig.getAttribute('data-type'), - caption: videoFig.getAttribute('data-caption') - }); - } -}); diff --git a/static/assets/media/videos/tour-captions.vtt b/static/assets/media/videos/tour-captions.vtt new file mode 100644 index 0000000..0e770e0 --- /dev/null +++ b/static/assets/media/videos/tour-captions.vtt @@ -0,0 +1,4 @@ +WEBVTT + +00:00:00.000 --> 00:00:02.000 +No audio. Silent video tour.