This commit is contained in:
@@ -1,83 +1,100 @@
|
||||
<script lang="ts">
|
||||
import type { MediaItem } from '$lib/media.js';
|
||||
import type { MediaItem } from '$lib/media.js';
|
||||
|
||||
interface Props {
|
||||
index?: number;
|
||||
item: MediaItem;
|
||||
}
|
||||
interface Props {
|
||||
index?: number;
|
||||
item: MediaItem;
|
||||
showLightbox: (item: MediaItem) => void;
|
||||
}
|
||||
|
||||
let { item, index }: Props = $props();
|
||||
let { item, index, showLightbox }: Props = $props();
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
||||
<figure
|
||||
class="gallery-item{item.type === 'video' ? ' video' : ''}"
|
||||
tabindex="0"
|
||||
data-name={item.name}
|
||||
data-type={item.type}
|
||||
data-caption={item.caption}
|
||||
<button
|
||||
class={`gallery-item ${item.type === 'video' ? ' video' : ''}`}
|
||||
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);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<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, '')}
|
||||
height={item.height ?? undefined}
|
||||
width={item.width ?? undefined}
|
||||
loading={index && index > 2 ? item.loading ?? 'lazy' : undefined}
|
||||
fetchpriority={item.fetchpriority ?? undefined}
|
||||
/>
|
||||
</picture>
|
||||
<figcaption>{item.caption}</figcaption>
|
||||
</figure>
|
||||
<figure>
|
||||
<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, '')}
|
||||
height={item.height ?? undefined}
|
||||
width={item.width ?? undefined}
|
||||
loading={index && index > 2
|
||||
? (item.loading ?? 'lazy')
|
||||
: undefined}
|
||||
fetchpriority={item.fetchpriority ?? undefined}
|
||||
/>
|
||||
</picture>
|
||||
<figcaption>{item.caption}</figcaption>
|
||||
</figure>
|
||||
</button>
|
||||
|
||||
<style>
|
||||
.gallery-item {
|
||||
aspect-ratio: 3/2;
|
||||
margin: 0 0 1rem;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
|
||||
& img {
|
||||
height: auto;
|
||||
width: 100%;
|
||||
display: block;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
& figcaption {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
padding: 0.5rem;
|
||||
background: rgb(0 0 0 / 60%);
|
||||
color: #fff;
|
||||
font-size: 0.9rem;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
.gallery-item {
|
||||
aspect-ratio: 3/2;
|
||||
background: none;
|
||||
border: none;
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
margin: 0 0 1rem;
|
||||
position: relative;
|
||||
padding: 0;
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid var(--accent);
|
||||
}
|
||||
& figure {
|
||||
margin: 0;
|
||||
}
|
||||
& img {
|
||||
height: auto;
|
||||
width: 100%;
|
||||
display: block;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
&:focus,
|
||||
&:hover {
|
||||
& figcaption {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
& figcaption {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
padding: 0.5rem;
|
||||
background: rgb(0 0 0 / 60%);
|
||||
color: #fff;
|
||||
font-size: 0.9rem;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid var(--accent);
|
||||
}
|
||||
|
||||
&:focus,
|
||||
&:hover {
|
||||
& figcaption {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
140
src/lib/components/Lightbox.svelte
Normal file
140
src/lib/components/Lightbox.svelte
Normal file
@@ -0,0 +1,140 @@
|
||||
<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();
|
||||
}
|
||||
});
|
||||
</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>
|
||||
|
||||
<style>
|
||||
.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;
|
||||
|
||||
&::backdrop {
|
||||
background: var(--lightbox-backdrop);
|
||||
}
|
||||
|
||||
&[open] {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
& img,
|
||||
& video {
|
||||
max-width: 90vw;
|
||||
max-height: 80vh;
|
||||
border-radius: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
header {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding: 0.25rem 0.25rem 0 0;
|
||||
}
|
||||
|
||||
.lb-close {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 2rem;
|
||||
color: var(--fg);
|
||||
}
|
||||
|
||||
.lb-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
& img {
|
||||
max-width: 90vw;
|
||||
max-height: 80vh;
|
||||
}
|
||||
}
|
||||
|
||||
.lb-caption {
|
||||
color: var(--fg);
|
||||
margin-top: 0.5rem;
|
||||
text-align: center;
|
||||
max-width: 90vw;
|
||||
}
|
||||
</style>
|
||||
@@ -1,30 +1,47 @@
|
||||
<script lang="ts">
|
||||
import { theme } from '$lib/stores/theme.svelte.js';
|
||||
|
||||
const toggleTheme = () => {
|
||||
theme.set(theme.get() === 'light' ? 'dark' : 'light');
|
||||
};
|
||||
</script>
|
||||
|
||||
<header class="site-header">
|
||||
<h1>64 Armandine St #3 Boston, Massachusetts</h1>
|
||||
<div class="buttons">
|
||||
<button id="show_video" class="emoji-button" aria-label="Show video tour">
|
||||
🎥
|
||||
</button>
|
||||
<button id="theme-toggle" class="emoji-button" aria-label="Toggle light/dark theme">
|
||||
🌓
|
||||
</button>
|
||||
</div>
|
||||
<h1>64 Armandine St #3 Boston, Massachusetts</h1>
|
||||
<div class="buttons">
|
||||
<button
|
||||
id="show_video"
|
||||
class="emoji-button"
|
||||
aria-label="Show video tour"
|
||||
>
|
||||
🎥
|
||||
</button>
|
||||
<button
|
||||
id="theme-toggle"
|
||||
class="emoji-button"
|
||||
aria-label="Toggle light/dark theme"
|
||||
onclick={toggleTheme}
|
||||
>
|
||||
🌓
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<style>
|
||||
.site-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1rem;
|
||||
background: var(--bg);
|
||||
border-bottom: 1px solid #ccc;
|
||||
}
|
||||
.site-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1rem;
|
||||
background: var(--bg);
|
||||
border-bottom: 1px solid #ccc;
|
||||
}
|
||||
|
||||
.emoji-button {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 1.5rem;
|
||||
cursor: pointer;
|
||||
color: var(--accent);
|
||||
}
|
||||
.emoji-button {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 1.5rem;
|
||||
cursor: pointer;
|
||||
color: var(--accent);
|
||||
}
|
||||
</style>
|
||||
|
||||
350
src/lib/media.ts
350
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,
|
||||
},
|
||||
];
|
||||
|
||||
10
src/lib/stores/theme.svelte.ts
Normal file
10
src/lib/stores/theme.svelte.ts
Normal file
@@ -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);
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user