Add ViewModeSelector component with width controls

- Create ViewModeSelector component with masonry view mode button
- Add width toggle controls (normal 700px / wide 900px)
- Create width-normal and width-wide SVG icons
- Integrate component into photos route with smooth transitions
- Use SCSS variables throughout for consistent styling

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Justin Edmund 2025-06-18 10:26:14 +01:00
parent a69f1098de
commit b2488bd301
4 changed files with 136 additions and 1 deletions

View file

@ -0,0 +1,8 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<!-- Normal width container -->
<rect x="5" y="4" width="10" height="12" rx="1" stroke="currentColor" stroke-width="1.5" fill="none"/>
<!-- Content lines -->
<line x1="7" y1="7" x2="13" y2="7" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
<line x1="7" y1="10" x2="11" y2="10" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
<line x1="7" y1="13" x2="13" y2="13" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 579 B

View file

@ -0,0 +1,8 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<!-- Wide width container -->
<rect x="2" y="4" width="16" height="12" rx="1" stroke="currentColor" stroke-width="1.5" fill="none"/>
<!-- Content lines -->
<line x1="4" y1="7" x2="16" y2="7" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
<line x1="4" y1="10" x2="12" y2="10" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
<line x1="4" y1="13" x2="16" y2="13" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 577 B

View file

@ -0,0 +1,101 @@
<script lang="ts">
import { getContext } from 'svelte'
import PhotosIcon from '$icons/photos.svg?component'
import WidthNormalIcon from '$icons/width-normal.svg?component'
import WidthWideIcon from '$icons/width-wide.svg?component'
interface Props {
mode?: 'masonry'
width?: 'normal' | 'wide'
onWidthChange?: (width: 'normal' | 'wide') => void
}
let {
mode = 'masonry',
width = 'normal',
onWidthChange
}: Props = $props()
</script>
<div class="view-mode-selector">
<div class="mode-section">
<button class="mode-button selected" aria-label="Masonry view">
<PhotosIcon />
</button>
</div>
<div class="separator"></div>
<div class="width-section">
<button
class="mode-button"
class:selected={width === 'normal'}
aria-label="Normal width"
onclick={() => onWidthChange?.('normal')}
>
<WidthNormalIcon />
</button>
<button
class="mode-button"
class:selected={width === 'wide'}
aria-label="Wide width"
onclick={() => onWidthChange?.('wide')}
>
<WidthWideIcon />
</button>
</div>
</div>
<style lang="scss">
.view-mode-selector {
width: 100%;
background: $grey-100;
border-radius: $corner-radius-lg;
box-sizing: border-box;
padding: $unit;
display: flex;
justify-content: space-between;
align-items: center;
gap: $unit-2x;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.mode-section,
.width-section {
display: flex;
gap: $unit-half;
}
.separator {
flex: 1;
min-width: $unit-2x;
}
.mode-button {
display: flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
border: none;
background: transparent;
border-radius: $corner-radius-sm;
cursor: pointer;
transition: all 0.2s ease;
color: $grey-60;
&:hover {
background: $grey-95;
}
&.selected {
color: $red-60;
background: $salmon-pink;
}
:global(svg) {
width: 20px;
height: 20px;
}
}
</style>

View file

@ -1,6 +1,7 @@
<script lang="ts">
import MasonryPhotoGrid from '$components/MasonryPhotoGrid.svelte'
import LoadingSpinner from '$components/admin/LoadingSpinner.svelte'
import ViewModeSelector from '$components/ViewModeSelector.svelte'
import { InfiniteLoader, LoaderState } from 'svelte-infinite'
import { generateMetaTags } from '$lib/utils/metadata'
import { page } from '$app/stores'
@ -15,6 +16,7 @@
// Initialize state with server-side data
let allPhotoItems = $state<PhotoItem[]>(data.photoItems || [])
let currentOffset = $state(data.pagination?.limit || 20)
let containerWidth = $state<'normal' | 'wide'>('normal')
// Track loaded photo IDs to prevent duplicates
let loadedPhotoIds = $state(new Set(data.photoItems?.map((item) => item.id) || []))
@ -100,7 +102,7 @@
<link rel="canonical" href={metaTags.other.canonical} />
</svelte:head>
<div class="photos-container">
<div class="photos-container" class:wide={containerWidth === 'wide'}>
{#if error}
<div class="error-container">
<div class="error-message">
@ -116,6 +118,10 @@
</div>
</div>
{:else}
<ViewModeSelector
width={containerWidth}
onWidthChange={(width) => containerWidth = width}
/>
<MasonryPhotoGrid photoItems={allPhotoItems} />
<InfiniteLoader
@ -163,6 +169,18 @@
max-width: 700px;
margin: 0 auto;
padding: 0 $unit-3x;
transition: max-width 0.3s ease;
&.wide {
max-width: 900px;
}
:global(.view-mode-selector) {
margin-bottom: $unit-3x;
position: sticky;
top: $unit-2x;
z-index: 10;
}
@include breakpoint('phone') {
padding: 0 $unit-2x;