Better photos interface

This commit is contained in:
Justin Edmund 2025-06-13 05:02:34 -04:00
parent 09e83618c9
commit 1c1b930e34
4 changed files with 497 additions and 664 deletions

View file

@ -0,0 +1,230 @@
<script lang="ts">
import BackButton from './BackButton.svelte'
interface Props {
title?: string
caption?: string
description?: string
exifData?: any
createdAt?: string
backHref?: string
backLabel?: string
showBackButton?: boolean
class?: string
}
let {
title,
caption,
description,
exifData,
createdAt,
backHref,
backLabel,
showBackButton = false,
class: className = ''
}: Props = $props()
const formatDate = (dateString: string) => {
const date = new Date(dateString)
return date.toLocaleDateString('en-US', {
month: 'long',
day: 'numeric',
year: 'numeric'
})
}
const hasDetails = $derived(title || caption || description)
const hasMetadata = $derived(exifData || createdAt)
</script>
<div class="photo-metadata {className}">
{#if hasDetails}
<div class="photo-details">
{#if title}
<h1 class="photo-title">{title}</h1>
{/if}
{#if caption || description}
<p class="photo-description">{caption || description}</p>
{/if}
</div>
{/if}
{#if hasMetadata}
<div class="metadata-grid {hasDetails ? 'metadata-section' : ''}">
{#if exifData?.camera}
<div class="metadata-item">
<span class="metadata-label">Camera</span>
<span class="metadata-value">{exifData.camera}</span>
</div>
{/if}
{#if exifData?.lens}
<div class="metadata-item">
<span class="metadata-label">Lens</span>
<span class="metadata-value">{exifData.lens}</span>
</div>
{/if}
{#if exifData?.focalLength}
<div class="metadata-item">
<span class="metadata-label">Focal Length</span>
<span class="metadata-value">{exifData.focalLength}</span>
</div>
{/if}
{#if exifData?.aperture}
<div class="metadata-item">
<span class="metadata-label">Aperture</span>
<span class="metadata-value">{exifData.aperture}</span>
</div>
{/if}
{#if exifData?.shutterSpeed}
<div class="metadata-item">
<span class="metadata-label">Shutter Speed</span>
<span class="metadata-value">{exifData.shutterSpeed}</span>
</div>
{/if}
{#if exifData?.iso}
<div class="metadata-item">
<span class="metadata-label">ISO</span>
<span class="metadata-value">{exifData.iso}</span>
</div>
{/if}
{#if exifData?.dateTaken}
<div class="metadata-item">
<span class="metadata-label">Date Taken</span>
<span class="metadata-value">{formatDate(exifData.dateTaken)}</span>
</div>
{:else if createdAt}
<div class="metadata-item">
<span class="metadata-label">Date</span>
<span class="metadata-value">{formatDate(createdAt)}</span>
</div>
{/if}
{#if exifData?.location}
<div class="metadata-item">
<span class="metadata-label">Location</span>
<span class="metadata-value">{exifData.location}</span>
</div>
{/if}
</div>
{/if}
{#if showBackButton && backHref && backLabel}
<div class="card-footer">
<BackButton href={backHref} label={backLabel} />
</div>
{/if}
</div>
<style lang="scss">
@import '$styles/variables.scss';
@import '$styles/mixins.scss';
.photo-metadata {
background: $grey-100;
border: 1px solid $grey-90;
border-radius: $image-corner-radius;
padding: $unit-3x;
padding-bottom: $unit-2x;
max-width: 700px;
margin: 0 auto;
width: 100%;
box-sizing: border-box;
@include breakpoint('phone') {
padding: $unit-3x;
max-width: 100%;
}
}
.photo-details {
margin-bottom: $unit-4x;
padding-bottom: $unit-4x;
border-bottom: 1px solid $grey-90;
text-align: center;
@include breakpoint('phone') {
margin-bottom: $unit-3x;
padding-bottom: $unit-3x;
}
.photo-title {
font-size: 1.75rem;
font-weight: 600;
margin: 0 0 $unit-2x;
color: $grey-10;
@include breakpoint('phone') {
font-size: 1.25rem;
margin-bottom: $unit;
}
}
.photo-description {
font-size: 1rem;
color: $grey-30;
line-height: 1.6;
margin: 0;
@include breakpoint('phone') {
font-size: 0.875rem;
}
}
}
.metadata-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: $unit-3x;
@include breakpoint('phone') {
grid-template-columns: 1fr;
gap: $unit-2x;
}
&.metadata-section {
margin-bottom: $unit-4x;
@include breakpoint('phone') {
margin-bottom: $unit-3x;
}
}
}
.metadata-item {
display: flex;
flex-direction: column;
gap: $unit-half;
.metadata-label {
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: $grey-40;
}
.metadata-value {
font-size: 0.875rem;
color: $grey-10;
font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New',
monospace;
}
}
.card-footer {
display: flex;
justify-content: center;
@include breakpoint('phone') {
margin-top: $unit-3x;
}
}
</style>

View file

@ -0,0 +1,55 @@
<script lang="ts">
import Zoom from 'svelte-medium-image-zoom'
import 'svelte-medium-image-zoom/dist/styles.css'
interface Props {
src: string
alt?: string
title?: string
id?: string
class?: string
}
let { src, alt = '', title, id, class: className = '' }: Props = $props()
</script>
<div class="photo-view {className}">
{#key id || src}
<Zoom>
<img {src} alt={title || alt || 'Photo'} class="photo-image" />
</Zoom>
{/key}
</div>
<style lang="scss">
@import '$styles/variables.scss';
@import '$styles/mixins.scss';
.photo-view {
display: flex;
justify-content: center;
font-size: 0;
line-height: 0;
position: relative;
z-index: 1;
}
.photo-image {
display: block;
width: 100%;
height: auto;
max-width: 700px;
object-fit: contain;
border-radius: $image-corner-radius;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
@include breakpoint('phone') {
border-radius: $image-corner-radius;
}
}
// Hide the zoom library's close button
:global([data-smiz-btn-unzoom]) {
display: none !important;
}
</style>

View file

@ -1,8 +1,13 @@
<script lang="ts">
import BackButton from '$components/BackButton.svelte'
import PhotoView from '$components/PhotoView.svelte'
import PhotoMetadata from '$components/PhotoMetadata.svelte'
import { generateMetaTags, generateCreativeWorkJsonLd } from '$lib/utils/metadata'
import { page } from '$app/stores'
import { goto } from '$app/navigation'
import type { PageData } from './$types'
import ArrowLeft from '$icons/arrow-left.svg'
import ArrowRight from '$icons/arrow-right.svg'
let { data }: { data: PageData } = $props()
@ -11,14 +16,6 @@
const navigation = $derived(data.navigation)
const error = $derived(data.error)
const formatDate = (dateString: string) => {
const date = new Date(dateString)
return date.toLocaleDateString('en-US', {
month: 'long',
day: 'numeric',
year: 'numeric'
})
}
const formatExif = (exifData: any) => {
if (!exifData) return null
@ -47,6 +44,24 @@
const exif = $derived(photo ? formatExif(photo.exifData) : null)
const pageUrl = $derived($page.url.href)
// Parse EXIF data if available (same as photo detail page)
const exifData = $derived(
photo?.exifData && typeof photo.exifData === 'object' ? photo.exifData : null
)
// Debug: Log what data we have
$effect(() => {
if (photo) {
console.log('Photo data:', {
id: photo.id,
title: photo.title,
caption: photo.caption,
exifData: photo.exifData,
createdAt: photo.createdAt
})
}
})
// Generate metadata
const photoTitle = $derived(photo?.title || photo?.caption || `Photo ${navigation?.currentIndex}`)
@ -127,155 +142,56 @@
</div>
{:else}
<div class="photo-page">
<!-- Navigation Header -->
<header class="photo-header">
<nav class="breadcrumb">
<a href="/photos">Photos</a>
<span class="separator"></span>
<a href="/photos/{album.slug}">{album.title}</a>
<span class="separator"></span>
<span class="current">Photo {navigation.currentIndex} of {navigation.totalCount}</span>
</nav>
<div class="photo-content-wrapper">
<PhotoView
src={photo.url}
alt={photo.caption}
title={photo.title}
id={photo.id}
/>
<div class="photo-nav">
<!-- Adjacent Photos Navigation -->
<div class="adjacent-navigation">
{#if navigation.prevPhoto}
<a href="/photos/{album.slug}/{navigation.prevPhoto.id}" class="nav-btn prev">
<svg width="20" height="20" viewBox="0 0 20 20" fill="none">
<path
d="M12.5 15L7.5 10L12.5 5"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
Previous
</a>
{:else}
<div class="nav-btn disabled">
<svg width="20" height="20" viewBox="0 0 20 20" fill="none">
<path
d="M12.5 15L7.5 10L12.5 5"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
Previous
</div>
<button
class="nav-button prev"
onclick={() => goto(`/photos/${album.slug}/${navigation.prevPhoto.id}`)}
type="button"
aria-label="Previous photo"
>
<ArrowLeft class="nav-icon" />
</button>
{/if}
{#if navigation.nextPhoto}
<a href="/photos/{album.slug}/{navigation.nextPhoto.id}" class="nav-btn next">
Next
<svg width="20" height="20" viewBox="0 0 20 20" fill="none">
<path
d="M7.5 5L12.5 10L7.5 15"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</a>
{:else}
<div class="nav-btn disabled">
Next
<svg width="20" height="20" viewBox="0 0 20 20" fill="none">
<path
d="M7.5 5L12.5 10L7.5 15"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</div>
<button
class="nav-button next"
onclick={() => goto(`/photos/${album.slug}/${navigation.nextPhoto.id}`)}
type="button"
aria-label="Next photo"
>
<ArrowRight class="nav-icon" />
</button>
{/if}
</div>
</header>
</div>
<!-- Photo Display -->
<main class="photo-main">
<div class="photo-container">
<img
src={photo.url}
alt={photo.caption || photo.title || 'Photo'}
class="main-photo"
loading="eager"
/>
</div>
</main>
<!-- Photo Details -->
<aside class="photo-details">
<div class="details-content">
{#if photo.title}
<h1 class="photo-title">{photo.title}</h1>
{/if}
{#if photo.caption}
<p class="photo-caption">{photo.caption}</p>
{/if}
{#if photo.description}
<p class="photo-description">{photo.description}</p>
{/if}
{#if exif}
<div class="photo-exif">
<h3>Photo Details</h3>
{#if exif.camera}
<div class="exif-item">
<span class="label">Camera</span>
<span class="value">{exif.camera}</span>
</div>
{/if}
{#if exif.lens}
<div class="exif-item">
<span class="label">Lens</span>
<span class="value">{exif.lens}</span>
</div>
{/if}
{#if exif.settings}
<div class="exif-item">
<span class="label">Settings</span>
<span class="value">{exif.settings}</span>
</div>
{/if}
{#if exif.location}
<div class="exif-item">
<span class="label">Location</span>
<span class="value">{exif.location}</span>
</div>
{/if}
{#if exif.dateTaken}
<div class="exif-item">
<span class="label">Date Taken</span>
<span class="value">{formatDate(exif.dateTaken)}</span>
</div>
{/if}
</div>
{/if}
<div class="photo-actions">
<a href="/photos/{album.slug}" class="back-to-album">← Back to {album.title}</a>
</div>
</div>
</aside>
<PhotoMetadata
title={photo.title}
caption={photo.caption}
description={photo.description}
{exifData}
createdAt={photo.createdAt}
backHref={`/photos/${album.slug}`}
backLabel={`Back to ${album.title}`}
showBackButton={true}
/>
</div>
{/if}
<style lang="scss">
:global(main) {
padding: 0;
}
@import '$styles/variables.scss';
@import '$styles/mixins.scss';
.error-container {
display: flex;
@ -304,234 +220,100 @@
}
.photo-page {
min-height: 100vh;
display: grid;
grid-template-areas:
'header header'
'main details';
grid-template-columns: 1fr 400px;
grid-template-rows: auto 1fr;
width: 100%;
max-width: 1200px;
margin: 0 auto;
padding: 0 $unit-3x $unit-4x;
align-items: center;
display: flex;
flex-direction: column;
gap: $unit-2x;
box-sizing: border-box;
position: relative;
@include breakpoint('tablet') {
grid-template-areas:
'header'
'main'
'details';
grid-template-columns: 1fr;
grid-template-rows: auto 1fr auto;
max-width: 900px;
}
@include breakpoint('phone') {
padding: 0 $unit-2x $unit-2x;
gap: $unit;
}
}
.photo-header {
grid-area: header;
background: $grey-100;
border-bottom: 1px solid $grey-90;
padding: $unit-3x $unit-4x;
.photo-content-wrapper {
position: relative;
max-width: 700px;
width: 100%;
margin: 0 auto;
display: flex;
align-items: center;
}
// Adjacent Navigation
.adjacent-navigation {
position: absolute;
top: 0;
bottom: 0;
left: calc(-48px - #{$unit-2x});
right: calc(-48px - #{$unit-2x});
display: flex;
justify-content: space-between;
align-items: center;
pointer-events: none;
z-index: 100;
@include breakpoint('phone') {
padding: $unit-2x;
flex-direction: column;
gap: $unit-2x;
align-items: stretch;
// Hide on mobile and tablet
@include breakpoint('tablet') {
display: none;
}
}
.breadcrumb {
font-size: 0.875rem;
color: $grey-40;
a {
color: $grey-40;
text-decoration: none;
transition: color 0.2s ease;
&:hover {
color: $grey-20;
}
}
.separator {
margin: 0 $unit;
}
.current {
color: $grey-20;
}
}
.photo-nav {
display: flex;
gap: $unit-2x;
@include breakpoint('phone') {
justify-content: space-between;
}
}
.nav-btn {
.nav-button {
width: 48px;
height: 48px;
pointer-events: auto;
position: relative;
border: none;
padding: 0;
background: $grey-100;
cursor: pointer;
border-radius: 50%;
display: flex;
align-items: center;
gap: $unit;
padding: $unit $unit-2x;
border-radius: $unit;
border: 1px solid $grey-85;
background: $grey-100;
color: $grey-20;
text-decoration: none;
font-size: 0.875rem;
font-weight: 500;
justify-content: center;
transition: all 0.2s ease;
&:hover:not(.disabled) {
border-color: $grey-70;
background: $grey-95;
}
&.disabled {
opacity: 0.5;
cursor: not-allowed;
}
&.prev svg {
order: -1;
}
&.next svg {
order: 1;
}
}
.photo-main {
grid-area: main;
background: $grey-95;
display: flex;
align-items: center;
justify-content: center;
padding: $unit-4x;
min-height: 60vh;
@include breakpoint('tablet') {
min-height: 50vh;
}
@include breakpoint('phone') {
padding: $unit-2x;
}
}
.photo-container {
max-width: 100%;
max-height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.main-photo {
max-width: 100%;
max-height: 80vh;
width: auto;
height: auto;
border-radius: $unit;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
object-fit: contain;
@include breakpoint('tablet') {
max-height: 60vh;
}
}
.photo-details {
grid-area: details;
background: $grey-100;
border-left: 1px solid $grey-90;
overflow-y: auto;
@include breakpoint('tablet') {
border-left: none;
border-top: 1px solid $grey-90;
}
}
.details-content {
padding: $unit-4x;
@include breakpoint('phone') {
padding: $unit-3x $unit-2x;
}
}
.photo-title {
font-size: 1.5rem;
font-weight: 600;
margin: 0 0 $unit-2x;
color: $grey-10;
}
.photo-caption {
font-size: 1rem;
color: $grey-20;
margin: 0 0 $unit-3x;
line-height: 1.5;
}
.photo-description {
font-size: 1rem;
color: $grey-30;
margin: 0 0 $unit-4x;
line-height: 1.6;
}
.photo-exif {
margin-bottom: $unit-4x;
h3 {
font-size: 1rem;
font-weight: 600;
margin: 0 0 $unit-2x;
color: $grey-10;
}
}
.exif-item {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: $unit;
gap: $unit-2x;
.label {
font-size: 0.875rem;
color: $grey-50;
font-weight: 500;
flex-shrink: 0;
}
.value {
font-size: 0.875rem;
color: $grey-20;
text-align: right;
word-break: break-word;
}
}
.photo-actions {
padding-top: $unit-3x;
border-top: 1px solid $grey-90;
}
.back-to-album {
color: $grey-40;
text-decoration: none;
font-size: 0.925rem;
font-weight: 500;
transition: color 0.2s ease;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
&:hover {
color: $grey-20;
background: $grey-95;
transform: scale(1.1);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
}
&:focus-visible {
outline: none;
box-shadow:
0 0 0 3px $red-60,
0 0 0 5px $grey-100;
}
:global(svg) {
stroke: $grey-10;
width: 16px;
height: 16px;
fill: none;
stroke-width: 2px;
stroke-linecap: round;
stroke-linejoin: round;
}
&.prev {
margin-right: auto;
}
&.next {
margin-left: auto;
}
}
</style>

View file

@ -1,13 +1,15 @@
<script lang="ts">
import BackButton from '$components/BackButton.svelte'
import PhotoView from '$components/PhotoView.svelte'
import PhotoMetadata from '$components/PhotoMetadata.svelte'
import { generateMetaTags } from '$lib/utils/metadata'
import { page } from '$app/stores'
import { goto } from '$app/navigation'
import { onMount } from 'svelte'
import type { PageData } from './$types'
import { isAlbum } from '$lib/types/photos'
import Zoom from 'svelte-medium-image-zoom'
import 'svelte-medium-image-zoom/dist/styles.css'
import ArrowLeft from '$icons/arrow-left.svg'
import ArrowRight from '$icons/arrow-right.svg'
let { data }: { data: PageData } = $props()
@ -18,15 +20,6 @@
const pageUrl = $derived($page.url.href)
const formatDate = (dateString: string) => {
const date = new Date(dateString)
return date.toLocaleDateString('en-US', {
month: 'long',
day: 'numeric',
year: 'numeric'
})
}
// Generate metadata
const metaTags = $derived(
photo
@ -72,13 +65,13 @@
// Get previous and next photos (excluding albums)
const adjacentPhotos = $derived(() => {
if (!photoItems.length || !currentPhotoId) return { prev: null, next: null }
// Filter out albums - we only want photos
const photosOnly = photoItems.filter(item => !isAlbum(item))
const currentIndex = photosOnly.findIndex(item => item.id === currentPhotoId)
const photosOnly = photoItems.filter((item) => !isAlbum(item))
const currentIndex = photosOnly.findIndex((item) => item.id === currentPhotoId)
if (currentIndex === -1) return { prev: null, next: null }
return {
prev: currentIndex > 0 ? photosOnly[currentIndex - 1] : null,
next: currentIndex < photosOnly.length - 1 ? photosOnly[currentIndex + 1] : null
@ -142,136 +135,49 @@
{:else if photo}
<div class="photo-page">
<div class="photo-content-wrapper">
<div class="photo-container">
<Zoom>
<img src={photo.url} alt={photo.title || photo.caption || 'Photo'} class="photo-image" />
</Zoom>
</div>
<PhotoView
src={photo.url}
alt={photo.caption}
title={photo.title}
id={photo.id}
/>
<!-- Adjacent Photos Navigation (Desktop Only) -->
<div class="adjacent-photos">
<!-- Adjacent Photos Navigation -->
<div class="adjacent-navigation">
{#if adjacentPhotos().prev}
<button
class="adjacent-photo prev"
<button
class="nav-button prev"
onclick={() => navigateToPhoto(adjacentPhotos().prev)}
type="button"
aria-label="Previous photo"
>
<img
src={adjacentPhotos().prev.src}
alt={adjacentPhotos().prev.alt}
class="adjacent-image"
/>
<ArrowLeft class="nav-icon" />
</button>
{:else}
<div class="adjacent-placeholder"></div>
{/if}
{#if adjacentPhotos().next}
<button
class="adjacent-photo next"
<button
class="nav-button next"
onclick={() => navigateToPhoto(adjacentPhotos().next)}
type="button"
aria-label="Next photo"
>
<img
src={adjacentPhotos().next.src}
alt={adjacentPhotos().next.alt}
class="adjacent-image"
/>
<ArrowRight class="nav-icon" />
</button>
{:else}
<div class="adjacent-placeholder"></div>
{/if}
</div>
</div>
<div class="photo-info-card">
{#if photo.title || photo.caption || photo.description}
<div class="photo-details">
{#if photo.title}
<h1 class="photo-title">{photo.title}</h1>
{/if}
{#if photo.caption || photo.description}
<p class="photo-description">{photo.caption || photo.description}</p>
{/if}
</div>
{/if}
{#if exifData || photo.createdAt}
<div class="metadata-grid">
{#if exifData?.camera}
<div class="metadata-item">
<span class="metadata-label">Camera</span>
<span class="metadata-value">{exifData.camera}</span>
</div>
{/if}
{#if exifData?.lens}
<div class="metadata-item">
<span class="metadata-label">Lens</span>
<span class="metadata-value">{exifData.lens}</span>
</div>
{/if}
{#if exifData?.focalLength}
<div class="metadata-item">
<span class="metadata-label">Focal Length</span>
<span class="metadata-value">{exifData.focalLength}</span>
</div>
{/if}
{#if exifData?.aperture}
<div class="metadata-item">
<span class="metadata-label">Aperture</span>
<span class="metadata-value">{exifData.aperture}</span>
</div>
{/if}
{#if exifData?.shutterSpeed}
<div class="metadata-item">
<span class="metadata-label">Shutter Speed</span>
<span class="metadata-value">{exifData.shutterSpeed}</span>
</div>
{/if}
{#if exifData?.iso}
<div class="metadata-item">
<span class="metadata-label">ISO</span>
<span class="metadata-value">{exifData.iso}</span>
</div>
{/if}
{#if exifData?.dateTaken}
<div class="metadata-item">
<span class="metadata-label">Date Taken</span>
<span class="metadata-value">{formatDate(exifData.dateTaken)}</span>
</div>
{:else if photo.createdAt}
<div class="metadata-item">
<span class="metadata-label">Date</span>
<span class="metadata-value">{formatDate(photo.createdAt)}</span>
</div>
{/if}
{#if exifData?.location}
<div class="metadata-item">
<span class="metadata-label">Location</span>
<span class="metadata-value">{exifData.location}</span>
</div>
{/if}
</div>
{/if}
</div>
<!-- Navigation Footer -->
<div class="navigation-footer">
<BackButton
href={photo.album ? `/photos/${photo.album.slug}` : '/photos'}
label={photo.album ? `Back to ${photo.album.title}` : 'Back to Photos'}
/>
</div>
<PhotoMetadata
title={photo.title}
caption={photo.caption}
description={photo.description}
{exifData}
createdAt={photo.createdAt}
backHref={photo.album ? `/photos/${photo.album.slug}` : '/photos'}
backLabel={photo.album ? `Back to ${photo.album.title}` : 'Back to Photos'}
showBackButton={true}
/>
</div>
{/if}
@ -312,6 +218,7 @@
flex-direction: column;
gap: $unit-2x;
box-sizing: border-box;
position: relative;
@include breakpoint('tablet') {
max-width: 900px;
@ -327,134 +234,24 @@
position: relative;
max-width: 700px;
width: 100%;
}
.photo-container {
max-width: 700px;
width: 100%;
font-size: 0;
line-height: 0;
position: relative;
.photo-image {
display: block;
max-width: 100%;
width: 100%;
height: auto;
object-fit: contain;
border-radius: $image-corner-radius;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
@include breakpoint('phone') {
border-radius: $image-corner-radius;
}
}
}
.photo-info-card {
background: $grey-100;
border: 1px solid $grey-90;
border-radius: $image-corner-radius;
padding: $unit-4x;
max-width: 700px;
margin: 0 auto;
width: 100%;
box-sizing: border-box;
@include breakpoint('phone') {
padding: $unit-3x;
max-width: 100%;
}
}
.photo-details {
margin-bottom: $unit-4x;
padding-bottom: $unit-4x;
border-bottom: 1px solid $grey-90;
text-align: center;
@include breakpoint('phone') {
margin-bottom: $unit-3x;
padding-bottom: $unit-3x;
}
.photo-title {
font-size: 1.75rem;
font-weight: 600;
margin: 0 0 $unit-2x;
color: $grey-10;
@include breakpoint('phone') {
font-size: 1.25rem;
margin-bottom: $unit;
}
}
.photo-description {
font-size: 1rem;
color: $grey-30;
line-height: 1.6;
margin: 0;
@include breakpoint('phone') {
font-size: 0.875rem;
}
}
}
.metadata-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: $unit-3x;
@include breakpoint('phone') {
grid-template-columns: 1fr;
gap: $unit-2x;
}
}
.metadata-item {
display: flex;
flex-direction: column;
gap: $unit-half;
.metadata-label {
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: $grey-40;
}
.metadata-value {
font-size: 0.875rem;
color: $grey-10;
font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New',
monospace;
}
align-items: center;
}
// Navigation Footer
.navigation-footer {
display: flex;
justify-content: center;
max-width: 700px;
margin: 0 auto;
width: 100%;
}
// Adjacent Photos Navigation
.adjacent-photos {
// Adjacent Navigation
.adjacent-navigation {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: calc(100% + 400px); // Add space for the adjacent images
top: 0;
bottom: 0;
left: calc(-48px - #{$unit-2x});
right: calc(-48px - #{$unit-2x});
display: flex;
justify-content: space-between;
align-items: center;
pointer-events: none;
z-index: 10;
z-index: 100;
// Hide on mobile and tablet
@include breakpoint('tablet') {
@ -462,82 +259,51 @@
}
}
.adjacent-photo,
.adjacent-placeholder {
width: 200px;
height: 300px;
.nav-button {
width: 48px;
height: 48px;
pointer-events: auto;
}
.adjacent-photo {
position: relative;
border: none;
padding: 0;
background: none;
background: $grey-100;
cursor: pointer;
border-radius: $image-corner-radius;
overflow: hidden;
transition: all 0.3s ease;
opacity: 0.3;
filter: grayscale(100%);
&.prev {
transform: translateX(-25%);
}
&.next {
transform: translateX(25%);
}
&::before {
content: '';
position: absolute;
inset: -3px;
border-radius: $image-corner-radius;
border: 3px solid transparent;
z-index: 2;
pointer-events: none;
transition: border-color 0.2s ease;
}
&::after {
content: '';
position: absolute;
inset: 0;
border-radius: $image-corner-radius;
border: 2px solid transparent;
z-index: 3;
pointer-events: none;
transition: border-color 0.2s ease;
}
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
&:hover {
opacity: 0.6;
filter: grayscale(50%);
transform: scale(1.02) translateX(-25%);
&.next {
transform: scale(1.02) translateX(25%);
}
background: $grey-95;
transform: scale(1.1);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
}
&:focus-visible {
outline: none;
box-shadow:
0 0 0 3px $red-60,
0 0 0 5px $grey-100;
}
&::before {
border-color: $red-60;
}
:global(svg) {
stroke: $grey-10;
width: 16px;
height: 16px;
fill: none;
stroke-width: 2px;
stroke-linecap: round;
stroke-linejoin: round;
}
&::after {
border-color: $grey-100;
}
&.prev {
margin-right: auto;
}
&.next {
margin-left: auto;
}
}
.adjacent-image {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
</style>