@simple-photo-gallery/theme-modern 2.0.17 → 2.0.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/features/themes/base-theme/components/hero/Hero.astro +41 -49
- package/src/features/themes/base-theme/layouts/MainHead.astro +56 -20
- package/src/features/themes/base-theme/layouts/MainLayout.astro +5 -2
- package/src/features/themes/base-theme/pages/index.astro +4 -1
- package/src/features/themes/base-theme/utils/index.ts +43 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@simple-photo-gallery/theme-modern",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.18",
|
|
4
4
|
"description": "Modern theme for Simple Photo Gallery",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Vladimir Haltakov, Tomasz Rusin",
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"@eslint/eslintrc": "^3.3.1",
|
|
38
38
|
"@eslint/js": "^9.30.1",
|
|
39
|
-
"@simple-photo-gallery/common": "1.0.
|
|
39
|
+
"@simple-photo-gallery/common": "1.0.6",
|
|
40
40
|
"@types/photoswipe": "^4.1.6",
|
|
41
41
|
"@typescript-eslint/eslint-plugin": "^8.35.1",
|
|
42
42
|
"@typescript-eslint/parser": "^8.35.1",
|
|
@@ -1,21 +1,25 @@
|
|
|
1
1
|
---
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
|
|
4
|
-
import { getPhotoPath } from '../../utils';
|
|
4
|
+
import { buildHeroSrcset, getPhotoPath, LANDSCAPE_SIZES, PORTRAIT_SIZES } from '../../utils';
|
|
5
5
|
|
|
6
6
|
import HeroScrollToGalleryBtn from '@/features/themes/base-theme/components/hero/HeroScrollToGalleryBtn.astro';
|
|
7
7
|
import { renderMarkdown } from '@/lib/markdown';
|
|
8
8
|
|
|
9
|
+
import type { HeaderImageVariants } from '@simple-photo-gallery/common';
|
|
10
|
+
|
|
9
11
|
interface Props {
|
|
10
12
|
title: string;
|
|
11
13
|
description?: string;
|
|
12
14
|
thumbsBaseUrl?: string;
|
|
13
15
|
headerImage?: string;
|
|
14
16
|
headerImageBlurHash?: string;
|
|
17
|
+
headerImageVariants?: HeaderImageVariants;
|
|
15
18
|
mediaBaseUrl?: string;
|
|
16
19
|
}
|
|
17
20
|
|
|
18
|
-
const { title, description, thumbsBaseUrl, headerImage, headerImageBlurHash, mediaBaseUrl } =
|
|
21
|
+
const { title, description, thumbsBaseUrl, headerImage, headerImageBlurHash, headerImageVariants, mediaBaseUrl } =
|
|
22
|
+
Astro.props;
|
|
19
23
|
|
|
20
24
|
// Parse description as Markdown if it exists
|
|
21
25
|
const parsedDescription: string = description ? await renderMarkdown(description) : '';
|
|
@@ -28,59 +32,47 @@ const thumbnailBasePath = thumbsBaseUrl || 'gallery/images';
|
|
|
28
32
|
|
|
29
33
|
// Original header photo
|
|
30
34
|
const headerPhotoPath = getPhotoPath(headerImage || '', mediaBaseUrl);
|
|
35
|
+
|
|
36
|
+
// Determine which sources to show based on headerImageVariants
|
|
37
|
+
// If headerImageVariants is not set, use all generated paths (default behavior)
|
|
38
|
+
// If headerImageVariants is set, only show sources for formats that are explicitly provided
|
|
39
|
+
const useDefaultPaths = !headerImageVariants;
|
|
40
|
+
|
|
41
|
+
// Pre-compute all srcsets - only render sources when srcset is non-empty
|
|
42
|
+
// This handles edge cases like empty format objects: { avif: {} }
|
|
43
|
+
const portraitAvifSrcset = buildHeroSrcset(headerImageVariants?.portrait?.avif, PORTRAIT_SIZES, thumbnailBasePath, imgBasename, 'portrait', 'avif', useDefaultPaths);
|
|
44
|
+
const portraitJpgSrcset = buildHeroSrcset(headerImageVariants?.portrait?.jpg, PORTRAIT_SIZES, thumbnailBasePath, imgBasename, 'portrait', 'jpg', useDefaultPaths);
|
|
45
|
+
const landscapeAvifSrcset = buildHeroSrcset(headerImageVariants?.landscape?.avif, LANDSCAPE_SIZES, thumbnailBasePath, imgBasename, 'landscape', 'avif', useDefaultPaths);
|
|
46
|
+
const landscapeJpgSrcset = buildHeroSrcset(headerImageVariants?.landscape?.jpg, LANDSCAPE_SIZES, thumbnailBasePath, imgBasename, 'landscape', 'jpg', useDefaultPaths);
|
|
31
47
|
---
|
|
32
48
|
|
|
33
49
|
<section class="hero">
|
|
34
50
|
<div class="hero__bg-wrapper">
|
|
35
51
|
{headerImageBlurHash && <canvas data-blur-hash={headerImageBlurHash} width={32} height={32} />}
|
|
36
52
|
<picture class="hero__bg" id="hero-bg-picture">
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
/>
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
<source
|
|
61
|
-
type="image/avif"
|
|
62
|
-
srcset={`
|
|
63
|
-
${thumbnailBasePath}/${imgBasename}_landscape_640.avif 640w,
|
|
64
|
-
${thumbnailBasePath}/${imgBasename}_landscape_960.avif 960w,
|
|
65
|
-
${thumbnailBasePath}/${imgBasename}_landscape_1280.avif 1280w,
|
|
66
|
-
${thumbnailBasePath}/${imgBasename}_landscape_1920.avif 1920w,
|
|
67
|
-
${thumbnailBasePath}/${imgBasename}_landscape_2560.avif 2560w,
|
|
68
|
-
${thumbnailBasePath}/${imgBasename}_landscape_3840.avif 3840w`}
|
|
69
|
-
sizes="100vw"
|
|
70
|
-
/>
|
|
71
|
-
<source
|
|
72
|
-
type="image/jpg"
|
|
73
|
-
srcset={`
|
|
74
|
-
${thumbnailBasePath}/${imgBasename}_landscape_640.jpg 640w,
|
|
75
|
-
${thumbnailBasePath}/${imgBasename}_landscape_960.jpg 960w,
|
|
76
|
-
${thumbnailBasePath}/${imgBasename}_landscape_1280.jpg 1280w,
|
|
77
|
-
${thumbnailBasePath}/${imgBasename}_landscape_1920.jpg 1920w,
|
|
78
|
-
${thumbnailBasePath}/${imgBasename}_landscape_2560.jpg 2560w,
|
|
79
|
-
${thumbnailBasePath}/${imgBasename}_landscape_3840.jpg 3840w`}
|
|
80
|
-
sizes="100vw"
|
|
81
|
-
/>
|
|
82
|
-
|
|
83
|
-
<!-- Fallback -->
|
|
53
|
+
{/* Portrait */}
|
|
54
|
+
{portraitAvifSrcset && (
|
|
55
|
+
<source
|
|
56
|
+
type="image/avif"
|
|
57
|
+
media="(max-aspect-ratio: 3/4)"
|
|
58
|
+
srcset={portraitAvifSrcset}
|
|
59
|
+
sizes="(max-aspect-ratio: 3/4) 160vw, 100vw"
|
|
60
|
+
/>
|
|
61
|
+
)}
|
|
62
|
+
{portraitJpgSrcset && (
|
|
63
|
+
<source
|
|
64
|
+
type="image/jpeg"
|
|
65
|
+
media="(max-aspect-ratio: 3/4)"
|
|
66
|
+
srcset={portraitJpgSrcset}
|
|
67
|
+
sizes="(max-aspect-ratio: 3/4) 160vw, 100vw"
|
|
68
|
+
/>
|
|
69
|
+
)}
|
|
70
|
+
|
|
71
|
+
{/* Landscape */}
|
|
72
|
+
{landscapeAvifSrcset && <source type="image/avif" srcset={landscapeAvifSrcset} sizes="100vw" />}
|
|
73
|
+
{landscapeJpgSrcset && <source type="image/jpeg" srcset={landscapeJpgSrcset} sizes="100vw" />}
|
|
74
|
+
|
|
75
|
+
{/* Fallback */}
|
|
84
76
|
<img src={headerPhotoPath} class="hero__bg-img" />
|
|
85
77
|
</picture>
|
|
86
78
|
<script is:inline>
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
---
|
|
2
|
-
import
|
|
2
|
+
import { buildHeroSrcset, LANDSCAPE_SIZES, PORTRAIT_SIZES } from '../utils';
|
|
3
|
+
|
|
4
|
+
import type { GalleryMetadata, HeaderImageVariants } from '@simple-photo-gallery/common/src/gallery';
|
|
3
5
|
|
|
4
6
|
interface Props {
|
|
5
7
|
title: string;
|
|
@@ -8,15 +10,45 @@ interface Props {
|
|
|
8
10
|
thumbsBaseUrl?: string;
|
|
9
11
|
metadata?: GalleryMetadata;
|
|
10
12
|
headerImageBasename?: string;
|
|
13
|
+
headerImageVariants?: HeaderImageVariants;
|
|
11
14
|
}
|
|
12
15
|
|
|
13
|
-
const { title, description, url, thumbsBaseUrl, metadata, headerImageBasename } = Astro.props;
|
|
16
|
+
const { title, description, url, thumbsBaseUrl, metadata, headerImageBasename, headerImageVariants } = Astro.props;
|
|
14
17
|
|
|
15
18
|
// Use headerImageBasename for dynamic image paths, fallback to generic name
|
|
16
19
|
const imgBasename = headerImageBasename || 'header';
|
|
17
20
|
|
|
18
21
|
// Get the base path for the thumbnails
|
|
19
22
|
const thumbnailBasePath = thumbsBaseUrl || 'gallery/images';
|
|
23
|
+
|
|
24
|
+
// Determine which sources to show based on headerImageVariants
|
|
25
|
+
// If headerImageVariants is not set, use all generated paths (default behavior)
|
|
26
|
+
// If headerImageVariants is set, only show sources for formats that are explicitly provided
|
|
27
|
+
const useDefaultPaths = !headerImageVariants;
|
|
28
|
+
|
|
29
|
+
// Pre-compute srcsets - only non-empty srcsets will be rendered
|
|
30
|
+
// This handles edge cases like empty format objects: { avif: {} }
|
|
31
|
+
const portraitAvifSrcset = buildHeroSrcset(headerImageVariants?.portrait?.avif, PORTRAIT_SIZES, thumbnailBasePath, imgBasename, 'portrait', 'avif', useDefaultPaths);
|
|
32
|
+
const portraitJpgSrcset = buildHeroSrcset(headerImageVariants?.portrait?.jpg, PORTRAIT_SIZES, thumbnailBasePath, imgBasename, 'portrait', 'jpg', useDefaultPaths);
|
|
33
|
+
const landscapeAvifSrcset = buildHeroSrcset(headerImageVariants?.landscape?.avif, LANDSCAPE_SIZES, thumbnailBasePath, imgBasename, 'landscape', 'avif', useDefaultPaths);
|
|
34
|
+
const landscapeJpgSrcset = buildHeroSrcset(headerImageVariants?.landscape?.jpg, LANDSCAPE_SIZES, thumbnailBasePath, imgBasename, 'landscape', 'jpg', useDefaultPaths);
|
|
35
|
+
|
|
36
|
+
// Determine media queries for preloads to match actual <source> media attributes in Hero.astro
|
|
37
|
+
// Portrait <source> always has media="(max-aspect-ratio: 3/4)", so preload must use the same restriction
|
|
38
|
+
// Otherwise landscape devices would preload portrait images they'll never use (wasting bandwidth)
|
|
39
|
+
// Landscape sources have no media restriction in Hero.astro, but we add one here when ANY portrait source exists
|
|
40
|
+
// to prevent landscape images from being preloaded on portrait devices (which will use portrait sources)
|
|
41
|
+
// We check both AVIF and JPG because portrait devices will use whichever portrait format is available
|
|
42
|
+
const hasAnyPortraitSource = portraitAvifSrcset || portraitJpgSrcset;
|
|
43
|
+
const portraitPreloadMedia = '(max-aspect-ratio: 3/4)';
|
|
44
|
+
const landscapePreloadMedia = hasAnyPortraitSource ? 'not (max-aspect-ratio: 3/4)' : undefined;
|
|
45
|
+
|
|
46
|
+
// Determine which format to preload for each orientation
|
|
47
|
+
// Prefer AVIF when available (better compression), fall back to JPG for JPG-only configurations
|
|
48
|
+
const portraitPreloadSrcset = portraitAvifSrcset || portraitJpgSrcset;
|
|
49
|
+
const portraitPreloadType = portraitAvifSrcset ? 'image/avif' : 'image/jpeg';
|
|
50
|
+
const landscapePreloadSrcset = landscapeAvifSrcset || landscapeJpgSrcset;
|
|
51
|
+
const landscapePreloadType = landscapeAvifSrcset ? 'image/avif' : 'image/jpeg';
|
|
20
52
|
---
|
|
21
53
|
|
|
22
54
|
<head>
|
|
@@ -77,22 +109,26 @@ const thumbnailBasePath = thumbsBaseUrl || 'gallery/images';
|
|
|
77
109
|
href="https://fonts.googleapis.com/css2?family=Montserrat:ital,wght@0,100..900;1,100..900&display=swap"
|
|
78
110
|
rel="stylesheet"
|
|
79
111
|
/>
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
112
|
+
{portraitPreloadSrcset && (
|
|
113
|
+
<link
|
|
114
|
+
rel="preload"
|
|
115
|
+
as="image"
|
|
116
|
+
type={portraitPreloadType}
|
|
117
|
+
media={portraitPreloadMedia}
|
|
118
|
+
imagesrcset={portraitPreloadSrcset}
|
|
119
|
+
imagesizes="(max-aspect-ratio: 3/4) 160vw, 100vw"
|
|
120
|
+
fetchpriority="high"
|
|
121
|
+
/>
|
|
122
|
+
)}
|
|
123
|
+
{landscapePreloadSrcset && (
|
|
124
|
+
<link
|
|
125
|
+
rel="preload"
|
|
126
|
+
as="image"
|
|
127
|
+
type={landscapePreloadType}
|
|
128
|
+
media={landscapePreloadMedia}
|
|
129
|
+
imagesrcset={landscapePreloadSrcset}
|
|
130
|
+
imagesizes="100vw"
|
|
131
|
+
fetchpriority="high"
|
|
132
|
+
/>
|
|
133
|
+
)}
|
|
98
134
|
</head>
|
|
@@ -3,7 +3,7 @@ import path from 'node:path';
|
|
|
3
3
|
|
|
4
4
|
import MainHead from '@/features/themes/base-theme/layouts/MainHead.astro';
|
|
5
5
|
|
|
6
|
-
import type { GalleryMetadata } from '@simple-photo-gallery/common/src/gallery';
|
|
6
|
+
import type { GalleryMetadata, HeaderImageVariants } from '@simple-photo-gallery/common/src/gallery';
|
|
7
7
|
|
|
8
8
|
interface Props {
|
|
9
9
|
title: string;
|
|
@@ -13,9 +13,11 @@ interface Props {
|
|
|
13
13
|
metadata?: GalleryMetadata;
|
|
14
14
|
analyticsScript?: string;
|
|
15
15
|
headerImage?: string;
|
|
16
|
+
headerImageVariants?: HeaderImageVariants;
|
|
16
17
|
}
|
|
17
18
|
|
|
18
|
-
const { title, description, metadata, url, thumbsBaseUrl, analyticsScript, headerImage } =
|
|
19
|
+
const { title, description, metadata, url, thumbsBaseUrl, analyticsScript, headerImage, headerImageVariants } =
|
|
20
|
+
Astro.props;
|
|
19
21
|
|
|
20
22
|
// Extract basename from headerImage filename
|
|
21
23
|
const headerImageBasename = headerImage ? path.basename(headerImage, path.extname(headerImage)) : undefined;
|
|
@@ -30,6 +32,7 @@ const headerImageBasename = headerImage ? path.basename(headerImage, path.extnam
|
|
|
30
32
|
url={url}
|
|
31
33
|
thumbsBaseUrl={thumbsBaseUrl}
|
|
32
34
|
headerImageBasename={headerImageBasename}
|
|
35
|
+
headerImageVariants={headerImageVariants}
|
|
33
36
|
/>
|
|
34
37
|
<body>
|
|
35
38
|
<slot />
|
|
@@ -29,6 +29,7 @@ const {
|
|
|
29
29
|
analyticsScript,
|
|
30
30
|
headerImage,
|
|
31
31
|
headerImageBlurHash,
|
|
32
|
+
headerImageVariants,
|
|
32
33
|
ctaBanner,
|
|
33
34
|
} = gallery;
|
|
34
35
|
|
|
@@ -66,12 +67,14 @@ const showCtaBanner = ctaBanner === true;
|
|
|
66
67
|
url={url}
|
|
67
68
|
thumbsBaseUrl={thumbsBaseUrl}
|
|
68
69
|
analyticsScript={analyticsScript}
|
|
69
|
-
headerImage={headerImage}
|
|
70
|
+
headerImage={headerImage}
|
|
71
|
+
headerImageVariants={headerImageVariants}>
|
|
70
72
|
<Hero
|
|
71
73
|
title={title}
|
|
72
74
|
description={description}
|
|
73
75
|
headerImage={headerImage}
|
|
74
76
|
headerImageBlurHash={headerImageBlurHash}
|
|
77
|
+
headerImageVariants={headerImageVariants}
|
|
75
78
|
thumbsBaseUrl={thumbsBaseUrl}
|
|
76
79
|
mediaBaseUrl={mediaBaseUrl}
|
|
77
80
|
/>
|
|
@@ -62,3 +62,46 @@ export const getSubgalleryThumbnailPath = (subgalleryHeaderImagePath: string) =>
|
|
|
62
62
|
|
|
63
63
|
return path.join(subgalleryFolderName, 'gallery', 'thumbnails', photoBasename);
|
|
64
64
|
};
|
|
65
|
+
|
|
66
|
+
/** Portrait image sizes for responsive hero images */
|
|
67
|
+
export const PORTRAIT_SIZES = [360, 480, 720, 1080] as const;
|
|
68
|
+
|
|
69
|
+
/** Landscape image sizes for responsive hero images */
|
|
70
|
+
export const LANDSCAPE_SIZES = [640, 960, 1280, 1920, 2560, 3840] as const;
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Build a srcset string for responsive images.
|
|
74
|
+
* Uses custom paths from variants when provided, otherwise generates default paths.
|
|
75
|
+
*
|
|
76
|
+
* @param variants - Optional record mapping sizes to custom URLs
|
|
77
|
+
* @param sizes - Array of image widths to include
|
|
78
|
+
* @param thumbnailBasePath - Base path for generated thumbnails
|
|
79
|
+
* @param imgBasename - Image basename for generated paths
|
|
80
|
+
* @param orientation - 'portrait' or 'landscape'
|
|
81
|
+
* @param format - Image format ('avif' or 'jpg')
|
|
82
|
+
* @param useDefaultPaths - Whether to use generated paths when no custom variant exists
|
|
83
|
+
* @returns Comma-separated srcset string
|
|
84
|
+
*/
|
|
85
|
+
export const buildHeroSrcset = (
|
|
86
|
+
variants: Record<number, string | undefined> | undefined,
|
|
87
|
+
sizes: readonly number[],
|
|
88
|
+
thumbnailBasePath: string,
|
|
89
|
+
imgBasename: string,
|
|
90
|
+
orientation: 'portrait' | 'landscape',
|
|
91
|
+
format: 'avif' | 'jpg',
|
|
92
|
+
useDefaultPaths: boolean,
|
|
93
|
+
): string => {
|
|
94
|
+
return sizes
|
|
95
|
+
.map((size) => {
|
|
96
|
+
const customPath = variants?.[size];
|
|
97
|
+
if (customPath) {
|
|
98
|
+
return `${customPath} ${size}w`;
|
|
99
|
+
}
|
|
100
|
+
if (useDefaultPaths) {
|
|
101
|
+
return `${thumbnailBasePath}/${imgBasename}_${orientation}_${size}.${format} ${size}w`;
|
|
102
|
+
}
|
|
103
|
+
return null;
|
|
104
|
+
})
|
|
105
|
+
.filter(Boolean)
|
|
106
|
+
.join(', ');
|
|
107
|
+
};
|