@simple-photo-gallery/theme-modern 2.0.0 → 2.0.2

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/astro.config.ts CHANGED
@@ -21,5 +21,8 @@ export default defineConfig({
21
21
  define: {
22
22
  'process.env.GALLERY_JSON_PATH': JSON.stringify(sourceGalleryPath),
23
23
  },
24
+ ssr: {
25
+ noExternal: ['@simple-photo-gallery/common'],
26
+ },
24
27
  },
25
28
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@simple-photo-gallery/theme-modern",
3
- "version": "2.0.0",
3
+ "version": "2.0.2",
4
4
  "description": "Modern theme for Simple Photo Gallery",
5
5
  "license": "MIT",
6
6
  "author": "Vladimir Haltakov, Tomasz Rusin",
@@ -27,14 +27,16 @@
27
27
  "check": "npm run lint && npm run format"
28
28
  },
29
29
  "dependencies": {
30
- "@types/photoswipe": "^4.1.6",
31
30
  "astro": "^5.11.0",
32
31
  "astro-relative-links": "^0.4.2",
32
+ "blurhash": "^2.0.5",
33
33
  "photoswipe": "^5.4.4"
34
34
  },
35
35
  "devDependencies": {
36
36
  "@eslint/eslintrc": "^3.3.1",
37
37
  "@eslint/js": "^9.30.1",
38
+ "@simple-photo-gallery/common": "1.0.0",
39
+ "@types/photoswipe": "^4.1.6",
38
40
  "@typescript-eslint/eslint-plugin": "^8.35.1",
39
41
  "@typescript-eslint/parser": "^8.35.1",
40
42
  "eslint": "^9.30.1",
@@ -3,7 +3,7 @@ import Container from '@/features/themes/base-theme/components/container/Contain
3
3
  import GallerySectionHeader from '@/features/themes/base-theme/components/gallery-section/GallerySectionHeader.astro';
4
4
  import GallerySectionItem from '@/features/themes/base-theme/components/gallery-section/GallerySectionItem.astro';
5
5
 
6
- import type { GallerySection as GallerySectionType } from '@/features/themes/base-theme/types/gallery';
6
+ import type { GallerySection as GallerySectionType } from '@simple-photo-gallery/common/src/gallery';
7
7
 
8
8
  interface Props {
9
9
  section: GallerySectionType;
@@ -14,7 +14,7 @@ interface Props {
14
14
  const { section, sectionIndex, mediaBaseUrl } = Astro.props;
15
15
 
16
16
  const validImages = section.images.filter(
17
- (image) => image.width > 0 && image.height > 0 && image.thumbnail?.width > 0 && image.thumbnail?.height > 0,
17
+ (image) => image.width > 0 && image.height > 0 && (image.thumbnail?.width || 0) > 0 && (image.thumbnail?.height || 0) > 0,
18
18
  );
19
19
  ---
20
20
 
@@ -22,7 +22,11 @@ const validImages = section.images.filter(
22
22
  <Container>
23
23
  <GallerySectionHeader section={section} />
24
24
  <div class="gallery-section__gallery" id={`gallery-${sectionIndex}`}>
25
- {validImages.map((image) => <GallerySectionItem image={image} mediaBaseUrl={mediaBaseUrl} />)}
25
+ {
26
+ validImages.map((image) => (
27
+ <GallerySectionItem image={image} mediaBaseUrl={mediaBaseUrl} blurHash={image.thumbnail?.blurHash} />
28
+ ))
29
+ }
26
30
  </div>
27
31
  </Container>
28
32
  </section>
@@ -1,5 +1,5 @@
1
1
  ---
2
- import type { GallerySection as GallerySectionType } from '@/features/themes/base-theme/types/gallery';
2
+ import type { GallerySection as GallerySectionType } from '@simple-photo-gallery/common';
3
3
 
4
4
  interface Props {
5
5
  section: GallerySectionType;
@@ -1,14 +1,15 @@
1
1
  ---
2
2
  import { getPhotoPath, getRelativePath } from '@/features/themes/base-theme/utils';
3
3
 
4
- import type { GalleryImage } from '@/features/themes/base-theme/types/gallery';
4
+ import type { MediaFile } from '@simple-photo-gallery/common/src/gallery';
5
5
 
6
6
  interface Props {
7
- image: GalleryImage;
7
+ image: MediaFile;
8
8
  mediaBaseUrl?: string;
9
+ blurHash?: string;
9
10
  }
10
11
 
11
- const { image, mediaBaseUrl } = Astro.props;
12
+ const { image, mediaBaseUrl, blurHash } = Astro.props;
12
13
  ---
13
14
 
14
15
  <a
@@ -20,14 +21,20 @@ const { image, mediaBaseUrl } = Astro.props;
20
21
  data-pswp-type={image.type}
21
22
  data-pswp-caption={image.alt ? `<p class="image-caption">${image.alt}</p>` : ''}
22
23
  style={`--w: ${image.thumbnail?.width}; --h: ${image.thumbnail?.height}`}>
23
- <img
24
- src={getRelativePath(image.thumbnail?.path)}
25
- srcset={`${getRelativePath(image.thumbnail?.path)} 1x, ${getRelativePath(image.thumbnail?.pathRetina)} 2x`}
26
- alt={image.alt}
27
- loading="lazy"
28
- width={image.thumbnail?.width}
29
- height={image.thumbnail?.height}
30
- />
24
+ <canvas data-blur-hash={blurHash} width={32} height={32}></canvas>
25
+
26
+ {
27
+ image.thumbnail && (
28
+ <img
29
+ src={getRelativePath(image.thumbnail.path)}
30
+ srcset={`${getRelativePath(image.thumbnail.path)} 1x, ${getRelativePath(image.thumbnail.pathRetina)} 2x`}
31
+ alt={image.alt}
32
+ loading="lazy"
33
+ width={image.thumbnail.width}
34
+ height={image.thumbnail.height}
35
+ />
36
+ )
37
+ }
31
38
  </a>
32
39
 
33
40
  <style>
@@ -45,6 +52,8 @@ const { image, mediaBaseUrl } = Astro.props;
45
52
  box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
46
53
  transition: all 0.3s ease;
47
54
  cursor: pointer;
55
+ position: relative;
56
+ z-index: 1;
48
57
  }
49
58
 
50
59
  .gallery-section__item:hover {
@@ -57,12 +66,31 @@ const { image, mediaBaseUrl } = Astro.props;
57
66
  height: auto;
58
67
  display: block;
59
68
  transition: transform 0.5s ease;
69
+ position: relative;
70
+ z-index: 2;
71
+ color: transparent;
72
+ font-size: 0;
60
73
  }
61
74
 
62
75
  .gallery-section__item:hover img {
63
76
  transform: scale(1.05);
64
77
  }
65
78
 
79
+ .gallery-section__item canvas {
80
+ position: absolute;
81
+ top: 0;
82
+ left: 0;
83
+ z-index: 1;
84
+ width: 100%;
85
+ height: 100%;
86
+ display: block;
87
+ transition: transform 0.5s ease;
88
+ }
89
+
90
+ .gallery-section__item:hover canvas {
91
+ transform: scale(1.05);
92
+ }
93
+
66
94
  @media (min-width: 640px) {
67
95
  :root {
68
96
  --row-height: 8rem;
@@ -1,23 +1,66 @@
1
1
  ---
2
- import { DEFAULT_STATIC_ASSETS_PATH } from '@/config';
3
2
  import HeroScrollToGalleryBtn from '@/features/themes/base-theme/components/hero/HeroScrollToGalleryBtn.astro';
4
- import { getPhotoPath } from '@/features/themes/base-theme/utils';
5
3
 
6
4
  interface Props {
7
5
  title: string;
8
6
  description?: string;
9
- backgroundImage: string;
10
- mediaBaseUrl?: string;
11
7
  }
12
8
 
13
- const { title, description, backgroundImage, mediaBaseUrl } = Astro.props;
9
+ const { title, description } = Astro.props;
14
10
  ---
15
11
 
16
12
  <section class="hero">
17
- <div
18
- class="hero__bg"
19
- style={`background-image: linear-gradient(rgba(0, 0, 0, 0.4), rgba(0, 0, 0, 0.4)), url('${getPhotoPath(backgroundImage, mediaBaseUrl) || `${DEFAULT_STATIC_ASSETS_PATH}/images/hero-placeholder.png`}')`}>
20
- </div>
13
+ <picture class="hero__bg">
14
+ <!-- Portrait -->
15
+ <source
16
+ type="image/avif"
17
+ media="(max-aspect-ratio: 3/4)"
18
+ srcset="
19
+ gallery/images/header_portrait_360.avif 360w,
20
+ gallery/images/header_portrait_480.avif 480w,
21
+ gallery/images/header_portrait_720.avif 720w,
22
+ gallery/images/header_portrait_1080.avif 1080w"
23
+ sizes="(max-aspect-ratio: 3/4) 160vw, 100vw"
24
+ />
25
+ <source
26
+ type="image/jpeg"
27
+ media="(max-aspect-ratio: 3/4)"
28
+ srcset="
29
+ gallery/images/header_portrait_360.jpg 360w,
30
+ gallery/images/header_portrait_480.jpg 480w,
31
+ gallery/images/header_portrait_720.jpg 720w,
32
+ gallery/images/header_portrait_1080.jpg 1080w"
33
+ sizes="(max-aspect-ratio: 3/4) 160vw, 100vw"
34
+ />
35
+
36
+ <!-- Landscape -->
37
+ <source
38
+ type="image/avif"
39
+ srcset="
40
+ gallery/images/header_landscape_640.avif 640w,
41
+ gallery/images/header_landscape_960.avif 960w,
42
+ gallery/images/header_landscape_1280.avif 1280w,
43
+ gallery/images/header_landscape_1920.avif 1920w,
44
+ gallery/images/header_landscape_2560.avif 2560w,
45
+ gallery/images/header_landscape_3840.avif 3840w"
46
+ sizes="100vw"
47
+ />
48
+ <source
49
+ type="image/jpg"
50
+ srcset="
51
+ gallery/images/header_landscape_640.jpg 640w,
52
+ gallery/images/header_landscape_960.jpg 960w,
53
+ gallery/images/header_landscape_1280.jpg 1280w,
54
+ gallery/images/header_landscape_1920.jpg 1920w,
55
+ gallery/images/header_landscape_2560.jpg 2560w,
56
+ gallery/images/header_landscape_3840.jpg 3840w"
57
+ sizes="100vw"
58
+ />
59
+
60
+ <!-- Fallback -->
61
+ <img src="gallery/images/header_landscape_1920.jpg" alt={title} />
62
+ </picture>
63
+ <div class="hero__overlay"></div>
21
64
  <div class="hero__content">
22
65
  <h1 class="hero__title">{title}</h1>
23
66
  {description && <p class="hero__description">{description}</p>}
@@ -42,9 +85,23 @@ const { title, description, backgroundImage, mediaBaseUrl } = Astro.props;
42
85
  left: 0;
43
86
  width: 100%;
44
87
  height: 100%;
45
- background-size: cover;
46
- background-position: center;
47
- background-repeat: no-repeat;
88
+ }
89
+
90
+ .hero__bg img {
91
+ width: 100%;
92
+ height: 100%;
93
+ object-fit: cover;
94
+ object-position: center;
95
+ }
96
+
97
+ .hero__overlay {
98
+ position: absolute;
99
+ top: 0;
100
+ left: 0;
101
+ width: 100%;
102
+ height: 100%;
103
+ background-color: rgba(0, 0, 0, 0.4);
104
+ z-index: 5;
48
105
  }
49
106
 
50
107
  .hero__content {
@@ -1,5 +1,4 @@
1
1
  ---
2
- import { DEFAULT_STATIC_ASSETS_PATH } from '@/config';
3
2
  import Container from '@/features/themes/base-theme/components/container/Container.astro';
4
3
  import { getRelativePath, getSubgalleryThumbnailPath } from '@/features/themes/base-theme/utils';
5
4
 
@@ -11,20 +10,18 @@ interface SubGallery {
11
10
 
12
11
  interface Props {
13
12
  title?: string;
14
- description?: string;
15
13
  subGalleries: SubGallery[];
16
14
  }
17
15
 
18
- const { title, description, subGalleries } = Astro.props;
16
+ const { title, subGalleries } = Astro.props;
19
17
  ---
20
18
 
21
19
  <section class="sub-galleries">
22
20
  <Container>
23
21
  {
24
- (title || description) && (
22
+ (title) && (
25
23
  <div class="sub-galleries__header">
26
24
  {title && <h2 class="sub-galleries__header-title">{title}</h2>}
27
- {description && <p class="sub-galleries__header-description">{description}</p>}
28
25
  </div>
29
26
  )
30
27
  }
@@ -33,14 +30,7 @@ const { title, description, subGalleries } = Astro.props;
33
30
  subGalleries.map((subgallery) => (
34
31
  <a href={getRelativePath(subgallery.path)} class="sub-galleries__item">
35
32
  <div class="sub-galleries__item-image">
36
- <img
37
- src={
38
- getSubgalleryThumbnailPath(subgallery.headerImage) ||
39
- `${DEFAULT_STATIC_ASSETS_PATH}/images/gallery-placeholder.png`
40
- }
41
- alt={subgallery.title}
42
- loading="lazy"
43
- />
33
+ <img src={getSubgalleryThumbnailPath(subgallery.headerImage)} alt={subgallery.title} loading="lazy" />
44
34
  </div>
45
35
  <div class="sub-galleries__item-content">
46
36
  <h3 class="sub-galleries__item-title">{subgallery.title}</h3>
@@ -1,5 +1,5 @@
1
1
  ---
2
- import type { GalleryMetadata } from '@/features/themes/base-theme/types/gallery';
2
+ import type { GalleryMetadata } from '@simple-photo-gallery/common';
3
3
 
4
4
  interface Props {
5
5
  title: string;
@@ -69,4 +69,22 @@ const { title, description, url, metadata } = Astro.props;
69
69
  href="https://fonts.googleapis.com/css2?family=Montserrat:ital,wght@0,100..900;1,100..900&display=swap"
70
70
  rel="stylesheet"
71
71
  />
72
+ <link
73
+ rel="preload"
74
+ as="image"
75
+ type="image/avif"
76
+ media="(max-aspect-ratio: 3/4)"
77
+ imagesrcset="gallery/images/header_portrait_360.avif 360w, gallery/images/header_portrait_480.avif 480w, gallery/images/header_portrait_720.avif 720w, gallery/images/header_portrait_1080.avif 1080w"
78
+ imagesizes="(max-aspect-ratio: 3/4) 160vw, 100vw"
79
+ fetchpriority="high"
80
+ />
81
+ <link
82
+ rel="preload"
83
+ as="image"
84
+ type="image/avif"
85
+ media="(min-aspect-ratio: 3/4)"
86
+ imagesrcset="gallery/images/header_landscape_640.avif 640w, gallery/images/header_landscape_960.avif 960w, gallery/images/header_landscape_1280.avif 1280w, gallery/images/header_landscape_1920.avif 1920w, gallery/images/header_landscape_2560.avif 2560w, gallery/images/header_landscape_3840.avif 3840w"
87
+ imagesizes="100vw"
88
+ fetchpriority="high"
89
+ />
72
90
  </head>
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  import MainHead from '@/features/themes/base-theme/layouts/MainHead.astro';
3
3
 
4
- import type { GalleryMetadata } from '@/features/themes/base-theme/types/gallery';
4
+ import type { GalleryMetadata } from '@simple-photo-gallery/common';
5
5
 
6
6
  interface Props {
7
7
  title: string;
@@ -8,7 +8,7 @@ import PhotoSwipe from '@/features/themes/base-theme/components/lightbox/PhotoSw
8
8
  import SubGalleries from '@/features/themes/base-theme/components/sub-galleries/SubGalleries.astro';
9
9
  import MainLayout from '@/features/themes/base-theme/layouts/MainLayout.astro';
10
10
 
11
- import type { GalleryData } from '@/features/themes/base-theme/types/gallery';
11
+ import type { GalleryData } from '@simple-photo-gallery/common/src/gallery';
12
12
 
13
13
  // Dynamically import gallery.json from source path or fallback to local
14
14
  const galleryJsonPath = process.env.GALLERY_JSON_PATH || './gallery.json';
@@ -16,19 +16,35 @@ const galleryData = JSON.parse(fs.readFileSync(galleryJsonPath, 'utf8'));
16
16
 
17
17
  const gallery = galleryData as GalleryData;
18
18
 
19
- const { title, description, headerImage, metadata, sections, subGalleries, mediaBaseUrl, url } = gallery;
19
+ const { title, description, metadata, sections, subGalleries, mediaBaseUrl, url } = gallery;
20
20
  ---
21
21
 
22
+ <script>
23
+ import { decode } from 'blurhash';
24
+
25
+ const blurHashCanvases = document.querySelectorAll<HTMLCanvasElement>('canvas[data-blur-hash]');
26
+
27
+ for (const canvas of blurHashCanvases) {
28
+ const blurHashValue = canvas.dataset.blurHash;
29
+
30
+ if (blurHashValue) {
31
+ const pixels = decode(blurHashValue, 32, 32);
32
+ const ctx = canvas.getContext('2d');
33
+
34
+ if (pixels && ctx) {
35
+ const imageData = new ImageData(new Uint8ClampedArray(pixels), 32, 32);
36
+ ctx.putImageData(imageData, 0, 0);
37
+ }
38
+ }
39
+ }
40
+ </script>
41
+
22
42
  <MainLayout title={title} description={description} metadata={metadata} url={url}>
23
- <Hero title={title} description={description} backgroundImage={headerImage} mediaBaseUrl={mediaBaseUrl} />
43
+ <Hero title={title} description={description} />
24
44
 
25
45
  {
26
46
  subGalleries && subGalleries.galleries.length > 0 && (
27
- <SubGalleries
28
- title={subGalleries.title}
29
- description={subGalleries.description}
30
- subGalleries={subGalleries.galleries}
31
- />
47
+ <SubGalleries title={subGalleries.title} subGalleries={subGalleries.galleries} />
32
48
  )
33
49
  }
34
50
  {
Binary file
@@ -1 +0,0 @@
1
- export const DEFAULT_STATIC_ASSETS_PATH = 'gallery';
@@ -1,56 +0,0 @@
1
- export interface GalleryImage {
2
- path: string;
3
- alt?: string;
4
- width: number;
5
- height: number;
6
- type: 'image' | 'video';
7
- thumbnail: {
8
- path: string;
9
- pathRetina: string;
10
- width: number;
11
- height: number;
12
- };
13
- }
14
-
15
- export interface GallerySection {
16
- title?: string;
17
- description?: string;
18
- images: GalleryImage[];
19
- }
20
-
21
- export interface SubGallery {
22
- title: string;
23
- headerImage: string;
24
- path: string;
25
- }
26
-
27
- export interface GalleryMetadata {
28
- image?: string;
29
- imageWidth?: number;
30
- imageHeight?: number;
31
- ogUrl?: string;
32
- ogType?: string;
33
- ogSiteName?: string;
34
- twitterSite?: string;
35
- twitterCreator?: string;
36
- author?: string;
37
- keywords?: string;
38
- canonicalUrl?: string;
39
- language?: string;
40
- robots?: string;
41
- }
42
-
43
- export interface GalleryData {
44
- title: string;
45
- description: string;
46
- url?: string;
47
- headerImage: string;
48
- metadata?: GalleryMetadata;
49
- mediaBaseUrl?: string;
50
- sections: GallerySection[];
51
- subGalleries?: {
52
- title?: string;
53
- description?: string;
54
- galleries: SubGallery[];
55
- };
56
- }