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:
parent
a69f1098de
commit
b2488bd301
4 changed files with 136 additions and 1 deletions
8
src/assets/icons/width-normal.svg
Normal file
8
src/assets/icons/width-normal.svg
Normal 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 |
8
src/assets/icons/width-wide.svg
Normal file
8
src/assets/icons/width-wide.svg
Normal 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 |
101
src/lib/components/ViewModeSelector.svelte
Normal file
101
src/lib/components/ViewModeSelector.svelte
Normal 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>
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import MasonryPhotoGrid from '$components/MasonryPhotoGrid.svelte'
|
import MasonryPhotoGrid from '$components/MasonryPhotoGrid.svelte'
|
||||||
import LoadingSpinner from '$components/admin/LoadingSpinner.svelte'
|
import LoadingSpinner from '$components/admin/LoadingSpinner.svelte'
|
||||||
|
import ViewModeSelector from '$components/ViewModeSelector.svelte'
|
||||||
import { InfiniteLoader, LoaderState } from 'svelte-infinite'
|
import { InfiniteLoader, LoaderState } from 'svelte-infinite'
|
||||||
import { generateMetaTags } from '$lib/utils/metadata'
|
import { generateMetaTags } from '$lib/utils/metadata'
|
||||||
import { page } from '$app/stores'
|
import { page } from '$app/stores'
|
||||||
|
|
@ -15,6 +16,7 @@
|
||||||
// Initialize state with server-side data
|
// Initialize state with server-side data
|
||||||
let allPhotoItems = $state<PhotoItem[]>(data.photoItems || [])
|
let allPhotoItems = $state<PhotoItem[]>(data.photoItems || [])
|
||||||
let currentOffset = $state(data.pagination?.limit || 20)
|
let currentOffset = $state(data.pagination?.limit || 20)
|
||||||
|
let containerWidth = $state<'normal' | 'wide'>('normal')
|
||||||
|
|
||||||
// Track loaded photo IDs to prevent duplicates
|
// Track loaded photo IDs to prevent duplicates
|
||||||
let loadedPhotoIds = $state(new Set(data.photoItems?.map((item) => item.id) || []))
|
let loadedPhotoIds = $state(new Set(data.photoItems?.map((item) => item.id) || []))
|
||||||
|
|
@ -100,7 +102,7 @@
|
||||||
<link rel="canonical" href={metaTags.other.canonical} />
|
<link rel="canonical" href={metaTags.other.canonical} />
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<div class="photos-container">
|
<div class="photos-container" class:wide={containerWidth === 'wide'}>
|
||||||
{#if error}
|
{#if error}
|
||||||
<div class="error-container">
|
<div class="error-container">
|
||||||
<div class="error-message">
|
<div class="error-message">
|
||||||
|
|
@ -116,6 +118,10 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
|
<ViewModeSelector
|
||||||
|
width={containerWidth}
|
||||||
|
onWidthChange={(width) => containerWidth = width}
|
||||||
|
/>
|
||||||
<MasonryPhotoGrid photoItems={allPhotoItems} />
|
<MasonryPhotoGrid photoItems={allPhotoItems} />
|
||||||
|
|
||||||
<InfiniteLoader
|
<InfiniteLoader
|
||||||
|
|
@ -163,6 +169,18 @@
|
||||||
max-width: 700px;
|
max-width: 700px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 0 $unit-3x;
|
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') {
|
@include breakpoint('phone') {
|
||||||
padding: 0 $unit-2x;
|
padding: 0 $unit-2x;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue