From 191499125023a5c407f17cc5866d0993a6ddbfd4 Mon Sep 17 00:00:00 2001 From: Andras Schmelczer Date: Sun, 31 May 2026 13:06:47 +0100 Subject: [PATCH] TIdy up --- .forgejo/workflows/deploy.yml | 10 +- e2e/gallery.spec.ts | 21 ++ playwright.config.ts | 22 ++ public/android-chrome-192x192.png | Bin 0 -> 9244 bytes public/android-chrome-512x512.png | Bin 0 -> 34182 bytes public/apple-touch-icon.png | Bin 0 -> 8137 bytes public/favicon-16x16.png | Bin 0 -> 538 bytes public/favicon-32x32.png | Bin 0 -> 1040 bytes public/favicon.ico | Bin 0 -> 15406 bytes public/site.webmanifest | 17 +- src/index.html | 100 +++++-- src/index.scss | 440 ++++++++++++++++-------------- src/photo-catalog.json | 197 +++++++++++++ src/photo-types.ts | 28 ++ src/photos.test.ts | 129 +++++++++ src/photos.ts | 322 ++++++++++++++++------ stylelint.config.js | 9 + vitest.config.ts | 9 + webpack.config.js | 174 ------------ 19 files changed, 991 insertions(+), 487 deletions(-) create mode 100644 e2e/gallery.spec.ts create mode 100644 playwright.config.ts create mode 100644 public/android-chrome-192x192.png create mode 100644 public/android-chrome-512x512.png create mode 100644 public/apple-touch-icon.png create mode 100644 public/favicon-16x16.png create mode 100644 public/favicon-32x32.png create mode 100644 public/favicon.ico create mode 100644 src/photo-catalog.json create mode 100644 src/photo-types.ts create mode 100644 src/photos.test.ts create mode 100644 stylelint.config.js create mode 100644 vitest.config.ts delete mode 100644 webpack.config.js diff --git a/.forgejo/workflows/deploy.yml b/.forgejo/workflows/deploy.yml index 5ee3f2f..85a0c53 100644 --- a/.forgejo/workflows/deploy.yml +++ b/.forgejo/workflows/deploy.yml @@ -30,8 +30,11 @@ jobs: - name: Install dependencies run: npm ci - - name: Check - run: npm run check + - name: Lint and static checks + run: npm run lint:check + + - name: Unit tests + run: npm test - name: Build run: npm run build @@ -65,5 +68,4 @@ jobs: - name: Copy build to host pages mount run: | apt update && apt install -y rsync - mkdir -p /pages/photos - rsync -a --delete dist/ /pages/photos/ + rsync -a --delete --mkpath --chmod=ugo=rwx dist/ /pages/photos/ diff --git a/e2e/gallery.spec.ts b/e2e/gallery.spec.ts new file mode 100644 index 0000000..6b436e0 --- /dev/null +++ b/e2e/gallery.spec.ts @@ -0,0 +1,21 @@ +import { expect, test } from '@playwright/test'; + +test('renders the gallery and supports keyboard navigation', async ({ + page, +}) => { + await page.goto('/'); + + await expect(page.locator('.thumbnail')).toHaveCount(39); + await expect(page.locator('#frame-image')).toBeVisible(); + + const firstAlt = await page.locator('#frame-image').getAttribute('alt'); + + await page.locator('.thumbnail').first().focus(); + await page.keyboard.press('ArrowRight'); + + await expect(page.locator('.thumbnail[aria-current="true"]')).toHaveCount(1); + await expect(page.locator('#frame-image')).not.toHaveAttribute( + 'alt', + firstAlt ?? '' + ); +}); diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..d71063c --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,22 @@ +import { defineConfig, devices } from '@playwright/test'; + +export default defineConfig({ + testDir: 'e2e', + timeout: 30_000, + use: { + baseURL: 'http://127.0.0.1:4173', + trace: 'on-first-retry', + }, + webServer: { + command: 'npm run build && npm run preview', + url: 'http://127.0.0.1:4173', + reuseExistingServer: !process.env.CI, + timeout: 120_000, + }, + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], +}); diff --git a/public/android-chrome-192x192.png b/public/android-chrome-192x192.png new file mode 100644 index 0000000000000000000000000000000000000000..e69ae9699e2bccfa0c1721fdf7cb627393044c88 GIT binary patch literal 9244 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 literal 0 HcmV?d00001 diff --git a/public/android-chrome-512x512.png b/public/android-chrome-512x512.png new file mode 100644 index 0000000000000000000000000000000000000000..46214523321e62cbbe627439aec03638c1009f9b GIT binary patch literal 34182 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 G1OFc{6BWh) literal 0 HcmV?d00001 diff --git a/public/apple-touch-icon.png b/public/apple-touch-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..4a71769c927afb0eb3b40d60acb12a870b0213fc GIT binary patch literal 8137 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 literal 0 HcmV?d00001 diff --git a/public/favicon-32x32.png b/public/favicon-32x32.png new file mode 100644 index 0000000000000000000000000000000000000000..dba2959e61317367ae1818ae2e59f116a785b39b GIT binary patch literal 1040 zcmV+r1n>KaP)5T^qjw(H0e$yIXO-0_Fm?F-{=2( zpZEDc?}?y_@g|_O1wwc0H0}e{J0Z9YK)oX3r9qY8&x=i&&-#Xk^-?mjQT?&N!4`ux zy#zW0vKwH7O$>wND_zOxv8n;a0*80#hW?g_>;s@W0w+n}ldfcRpele^Abb?WdjM{6 zB%EJF@``TAlY7r<=PL#{;|o78f)7drm?kj+;YEo-h{^FtS)~=}w&fbF;+FIOQ_%69jE}!OTKqy3Wqim4KDYfCeB`2&@S9 zrW(?*`Z@1D3Qaq1&(B>F%E;BGrIC#S+5(LF`uix#mzI-F0lWst&Jw4cbdiVr$>=Xm zvtbKxeAj^cuI7yCf_t3?1TgC&A9o5-wiUS{0hA&TOZnft>*qMR(3sifME_!LQ3@&JjS&AC3a(U-K^I zKxcAr$Qpdq7wMzWS}!Mq32m)_D{jT+#+W}m0pQ_WKOobKO_?qKP47@|uUa=(yC5Q- zyx93*LfgGr8Mx^HH}?ugeUTmta|O3Lm=Y=jSDTh{MOHuO?V%Va))k@1vCjD5OPg{9 z=KXG&uf0&D0hO@f>f0fZKrk_Az9%JzBNv#v6TZ*`Duv$d^9E}Y0# z<$}WIJj5vgC0a2j0lC%MIfcf7&g98;mbJn-&Ol*P&Tg}wr$s?`vB#((VjRF$Ck1FdIRq~UTQ-OGNT5_$wvhgp^-;6NNTfa^K0000< KMNUMnLSTYXDD}kv literal 0 HcmV?d00001 diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..eb6309f5faec341525c3842596f994e2b7be90c5 GIT binary patch literal 15406 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 F{tGzBw~zn; literal 0 HcmV?d00001 diff --git a/public/site.webmanifest b/public/site.webmanifest index 7679ff5..7029842 100644 --- a/public/site.webmanifest +++ b/public/site.webmanifest @@ -7,9 +7,20 @@ "theme_color": "#a63446", "icons": [ { - "src": "favicon.svg", - "sizes": "64x64", - "type": "image/svg+xml" + "src": "android-chrome-192x192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "android-chrome-512x512.png", + "sizes": "512x512", + "type": "image/png" + }, + { + "src": "android-chrome-512x512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" } ] } diff --git a/src/index.html b/src/index.html index 79201b6..c3da238 100644 --- a/src/index.html +++ b/src/index.html @@ -1,24 +1,76 @@ - - - - - Photos - András Schmelczer - - - - - - - -
-

András Schmelczer

-
-
-
-
- opened photo -
-
-
- - + + + + + + Photos - András Schmelczer + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ +
+ +
+
+ +
+ +
+ +
+ +
+
+ + + + diff --git a/src/index.scss b/src/index.scss index aedb4e0..557b91c 100644 --- a/src/index.scss +++ b/src/index.scss @@ -1,239 +1,273 @@ -@mixin center-children() { - display: flex; - justify-content: center; - align-items: center; -} +$breakpoint: 900px; -@mixin portrait() { - @media (max-width: 900px) { - @content; - } +:root { + --color-text: #fff; + --color-ink: #202124; + --color-accent: #a63446; + --color-accent-strong: #7c2130; + --color-background: #eee; + --color-focus: #ffd166; + --border-width: 5px; + --space-xs: 0.375rem; + --space-s: 0.75rem; + --space-m: 1.5rem; } -@mixin landscape() { - @media (min-width: 900px) { - @content; - } -} - -$text-color: #ffffff; -$accent-color: #a63446; -$background-color: #eeeeee; -$border-width: 5px; -$normal-margin: 24px; -$small-margin: 6px; - *, *::before, *::after { box-sizing: border-box; - margin: 0; - padding: 0; -} - -:focus { - outline: $accent-color solid $border-width * 1.1; - outline-offset: -$border-width * 1.1; -} - -img { - border: $border-width solid $text-color; - box-sizing: content-box; - width: 100%; - height: auto; - display: block; } html { - height: 100%; - overflow-x: hidden; - background-color: $background-color; + min-height: 100%; + background: var(--color-background); + color: var(--color-ink); + font-family: + system-ui, + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + sans-serif; +} - body { - display: flex; - overflow: hidden; +body { + min-height: 100dvh; + margin: 0; + display: grid; + grid-template-columns: auto minmax(0, 1fr); +} - @include portrait { - height: 100%; - flex-direction: column; - } +img { + display: block; + max-inline-size: 100%; + block-size: auto; +} - @include landscape { - &::-webkit-scrollbar-track, - &::-webkit-scrollbar { - background-color: transparent; - width: 12px; - } - &::-webkit-scrollbar-thumb { - background-color: $accent-color; - border-radius: 1000px; - } - } - h1, - header { - font-family: 'Roboto', serif; - font-weight: 100; +:focus { + outline: none; +} - @include landscape { - font-size: 3rem; - } +:focus-visible { + outline: 3px solid var(--color-focus); + outline-offset: 4px; +} - @include portrait { - font-size: 2rem; - } - } +.site-header { + display: grid; + place-items: center; + background: var(--color-accent); + color: var(--color-text); + padding: var(--space-m) var(--space-s); +} - header { - @include center-children; - box-sizing: content-box; - height: 100%; - position: relative; +.site-header h1 { + margin: 0; + font-size: clamp(1.5rem, 4vmin, 3rem); + font-weight: 300; + letter-spacing: 0; + line-height: 1; + writing-mode: vertical-rl; + transform: rotate(180deg); + white-space: nowrap; +} - @include portrait { - width: 100%; - height: 2.15ch; - padding: $normal-margin 0; - } +.gallery { + min-width: 0; + min-height: 100dvh; + display: grid; + grid-template-columns: minmax(5rem, 10vmin) minmax(0, 1fr) minmax( + 7rem, + 14vmin + ); + position: relative; +} - @include landscape { - width: 2.15ch; - height: 100vh; - padding: 0 $normal-margin; - } +.rail { + min-height: 0; + max-height: 100dvh; + overflow-y: auto; + overscroll-behavior: contain; + display: flex; + flex-direction: column; + align-items: center; + gap: var(--space-s); + padding: var(--space-m) var(--space-s); + scrollbar-color: var(--color-accent) transparent; +} - h1 { - color: $text-color; - background-color: $accent-color; - padding: $normal-margin 0; +.thumbnail { + display: block; + flex: 0 0 auto; + border: var(--border-width) solid var(--color-text); + background: var(--color-text); + color: inherit; + box-shadow: 0 0 0 1px rgb(32 33 36 / 16%); +} - white-space: nowrap; - user-select: none; - z-index: 2; - position: fixed; +.thumbnail:hover, +.thumbnail.is-selected { + border-color: var(--color-accent); +} - text-align: center; +.thumbnail:focus-visible { + outline: 3px solid var(--color-focus); + outline-offset: 3px; + box-shadow: + 0 0 0 6px var(--color-accent-strong), + 0 0 0 1px rgb(32 33 36 / 16%); +} - @include portrait { - width: 100%; - } +.thumbnail--portrait { + inline-size: 8vmin; +} - @include landscape { - width: 101vh; - transform: rotate(-90deg); - } - } - } +.thumbnail--landscape { + inline-size: 12vmin; +} - main#images { - flex: 1; - display: flex; - justify-content: space-between; +.thumbnail picture, +.thumbnail img { + inline-size: 100%; +} - @include portrait { - flex-direction: column; - } +.frame { + min-width: 0; + min-height: 0; + display: grid; + grid-template-rows: minmax(0, 1fr) auto; + padding: var(--space-m); +} - .cluster { - display: flex; - justify-content: space-between; +.frame-content { + min-width: 0; + min-height: 0; + display: grid; + place-items: center; +} - img { - cursor: pointer; - } +.frame-figure { + min-width: 0; + margin: 0; + display: grid; + justify-items: center; + gap: var(--space-s); +} - &#portraits > img { - @include portrait { - height: 12vmin; - width: auto; - } - @include landscape { - width: 8vmin; - } - } +.frame-figure picture { + display: grid; + place-items: center; + min-width: 0; +} - &#landscapes > img { - @include portrait { - height: 8vmin; - width: auto; - } - @include landscape { - width: 12vmin; - } - } +.frame-figure img { + inline-size: auto; + max-inline-size: 100%; + max-block-size: calc(100dvh - 5rem); + border: var(--border-width) solid var(--color-text); + background: var(--color-text); + object-fit: contain; + box-shadow: 0 0 0 1px rgb(32 33 36 / 16%); +} - img { - @include portrait { - margin: 0 $small-margin; - } - } +.frame-figure figcaption { + max-inline-size: min(42rem, 100%); + color: var(--color-ink); + font-size: 0.95rem; + line-height: 1.35; + text-align: center; +} - @include portrait { - width: 100%; - overflow: auto; - margin: $small-margin 0; - padding: $small-margin 0; - object-fit: contain; +.slideshow-toggle { + display: none; + justify-self: end; + margin-block-start: var(--space-s); + border: 0; + border-radius: 4px; + padding: 0.45rem 0.75rem; + background: var(--color-accent); + color: var(--color-text); + font: inherit; + cursor: pointer; +} - :first-child { - margin-left: 2 * $small-margin; - } +.js .slideshow-toggle { + display: block; +} - &:last-child { - margin-right: 2 * $small-margin !important; - } - } +.slideshow-toggle:hover { + background: var(--color-accent-strong); +} - @include landscape { - flex-direction: column; - margin: $normal-margin; - - img { - margin: $small-margin 0; - } - - :first-child { - margin-top: 0; - } - - :last-child { - margin-bottom: 0; - } - } - } - - #frame-container { - @include center-children; - margin: 0 $border-width; - - flex: 1; - @include landscape { - } - - @include portrait { - position: relative; - } - - img { - height: auto; - width: auto; - - top: 50%; - transform: translateY(-50%) translateX(-50%); - - @include portrait { - position: absolute; - box-sizing: border-box; - max-width: 100% !important; // undo js - max-height: 100%; - left: 50% !important; // undo js - } - - @include landscape { - position: fixed; - max-height: 90vh; - } - } - } - } +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + scroll-behavior: auto !important; + } +} + +@media (width < $breakpoint) { + body { + min-height: 100dvh; + display: grid; + grid-template-columns: 1fr; + grid-template-rows: auto minmax(0, 1fr); + } + + .site-header { + padding: var(--space-s) var(--space-m); + } + + .site-header h1 { + writing-mode: horizontal-tb; + transform: none; + font-size: 2rem; + } + + .gallery { + min-height: 0; + grid-template-columns: minmax(0, 1fr); + grid-template-rows: auto minmax(0, 1fr) auto; + } + + .rail { + max-height: none; + min-width: 0; + overflow: auto hidden; + flex-direction: row; + justify-content: flex-start; + padding: var(--space-xs) var(--space-s); + } + + .rail--portraits { + grid-row: 1; + } + + .rail--landscapes { + grid-row: 3; + } + + .thumbnail { + inline-size: auto; + } + + .thumbnail--portrait img { + block-size: 12vmin; + inline-size: auto; + } + + .thumbnail--landscape img { + block-size: 8vmin; + inline-size: auto; + } + + .frame { + grid-row: 2; + padding: var(--space-s); + } + + .frame-figure img { + max-block-size: calc(100dvh - 12rem); } } diff --git a/src/photo-catalog.json b/src/photo-catalog.json new file mode 100644 index 0000000..3a61a8c --- /dev/null +++ b/src/photo-catalog.json @@ -0,0 +1,197 @@ +[ + { + "file": "_DSC1190-Edit.jpg", + "alt": "Portrait of a man in a burgundy sleeveless shirt standing with folded arms above a city view.", + "caption": "Portrait above the city" + }, + { + "file": "_DSC1484.jpg", + "alt": "A man smiling while drying a plate in a kitchen.", + "caption": "Kitchen portrait" + }, + { + "file": "_DSC1834.jpg", + "alt": "A person sitting near dark rocks while waves crash into a sea cave.", + "caption": "Watching the surf" + }, + { + "file": "_DSC2313.jpg", + "alt": "Portrait of a man in a teal coat standing among autumn leaves.", + "caption": "Autumn portrait" + }, + { + "file": "_DSC2362.jpg", + "alt": "Portrait of a man with glasses leaning on a railing in a park.", + "caption": "Park portrait" + }, + { + "file": "_DSC2674.jpg", + "alt": "A person in a hooded jacket walking down a narrow city street.", + "caption": "City walk" + }, + { + "file": "_DSC8718-Edit.jpg", + "alt": "Defocused city lights glowing across a dark night skyline.", + "caption": "Night lights" + }, + { + "file": "adam.jpg", + "alt": "A man standing against a dark background in a black graphic T-shirt.", + "caption": "Adam" + }, + { + "file": "dorka-1.jpg", + "alt": "A woman in a white knit hat sitting outdoors on an autumn day.", + "caption": "Dorka" + }, + { + "file": "kezdetazarovonalbol.jpg", + "alt": "A small plant stem silhouetted against warm yellow and orange light.", + "caption": "Small stem in warm light" + }, + { + "file": "nap-2.jpg", + "alt": "The sun setting behind a mountain ridge over a field.", + "caption": "Sunset field" + }, + { + "file": "nyomtatas-8.jpg", + "alt": "Close-up of ice crystals and frozen texture in blue light.", + "caption": "Ice crystals" + }, + { + "file": "nyomtatas-10.jpg", + "alt": "Warm abstract light passing through dried plants.", + "caption": "Dried plants in light" + }, + { + "file": "web-1-2.jpg", + "alt": "A city panorama seen through a coin-operated viewing telescope.", + "caption": "City lookout" + }, + { + "file": "web-1.jpg", + "alt": "Black and white stage scene with dancers performing before an audience.", + "caption": "Stage performance" + }, + { + "file": "web-2-2.jpg", + "alt": "Two zebras walking across a black and white landscape.", + "caption": "Zebras" + }, + { + "file": "web-2.jpg", + "alt": "Coastal town and hillside beside a blue sea under a bright sky.", + "caption": "Coastal town" + }, + { + "file": "web-3-2.jpg", + "alt": "A woman walking away on a beach while carrying a bag.", + "caption": "Beach walk" + }, + { + "file": "web-3.jpg", + "alt": "Black and white lake scene with a small boat under a bright sun.", + "caption": "Boat on the lake" + }, + { + "file": "web-4-2.jpg", + "alt": "Black and white seascape with waves rolling toward weathered posts.", + "caption": "Sea posts" + }, + { + "file": "web-5.jpg", + "alt": "Candles glowing in the foreground with soft golden lights behind them.", + "caption": "Candles" + }, + { + "file": "web-6.jpg", + "alt": "Aerial view over a city and river beneath broken clouds.", + "caption": "City and river" + }, + { + "file": "web-7.jpg", + "alt": "The Eiffel Tower lit at night against a dark sky.", + "caption": "Eiffel Tower at night" + }, + { + "file": "web-8.jpg", + "alt": "Bare winter trees fading into a pale snowy background.", + "caption": "Winter trees" + }, + { + "file": "web-10.jpg", + "alt": "A person in a dark jacket sitting on a bench and looking over water.", + "caption": "Looking across the water" + }, + { + "file": "web-11.jpg", + "alt": "Black and white close-up portrait of a dog looking into the camera.", + "caption": "Dog portrait" + }, + { + "file": "web-12.jpg", + "alt": "Two people sitting by water under a vivid orange sunset.", + "caption": "Sunset by the water" + }, + { + "file": "web-13.jpg", + "alt": "Portrait of a woman standing beside a tree in green summer light.", + "caption": "Tree portrait" + }, + { + "file": "web-14.jpg", + "alt": "Golden sunlight reflecting on rippled water.", + "caption": "Sunlit ripples" + }, + { + "file": "web-17.jpg", + "alt": "A small green plant shoot backlit by bright sunlight.", + "caption": "Backlit sprout" + }, + { + "file": "web-18.jpg", + "alt": "Birds flying around pale stone sculpture details.", + "caption": "Birds and stone" + }, + { + "file": "web-19.jpg", + "alt": "A bridge at night with blue lights and long yellow light trails.", + "caption": "Bridge light trails" + }, + { + "file": "web-20.jpg", + "alt": "A seated man in a dark coat photographed at night with city lights behind him.", + "caption": "Night portrait" + }, + { + "file": "web-22.jpg", + "alt": "Silhouettes seen through large windows during a pink and blue sunset.", + "caption": "Window silhouettes" + }, + { + "file": "web-23.jpg", + "alt": "Tall clock tower and stone architecture with a bird flying nearby.", + "caption": "Clock tower" + }, + { + "file": "web-24.jpg", + "alt": "Portrait of a woman at night with bridge lights behind her.", + "caption": "Bridge portrait" + }, + { + "file": "web-25.jpg", + "alt": "Portrait of a smiling man at night with blurred city lights behind him.", + "caption": "Smiling night portrait" + }, + { + "file": "web-26.jpg", + "alt": "Tall bridge lamps receding into a pale sky.", + "caption": "Bridge lamps" + }, + { + "file": "web-42.jpg", + "alt": "Warm sunset over a grassy field with low hills in the distance.", + "caption": "Sunset hills" + } +] diff --git a/src/photo-types.ts b/src/photo-types.ts new file mode 100644 index 0000000..d9cf124 --- /dev/null +++ b/src/photo-types.ts @@ -0,0 +1,28 @@ +export type PhotoOrientation = 'landscape' | 'portrait'; +export type ImageMimeType = 'image/avif' | 'image/webp' | 'image/jpeg'; + +export type ImageVariant = { + readonly src: string; + readonly width: number; + readonly height: number; +}; + +export type ImageSource = { + readonly type: ImageMimeType; + readonly variants: readonly ImageVariant[]; + readonly srcset: string; +}; + +export type Photo = { + readonly id: string; + readonly file: string; + readonly order: number; + readonly alt: string; + readonly caption: string; + readonly orientation: PhotoOrientation; + readonly width: number; + readonly height: number; + readonly aspectRatio: number; + readonly sources: readonly ImageSource[]; + readonly fallback: ImageVariant; +}; diff --git a/src/photos.test.ts b/src/photos.test.ts new file mode 100644 index 0000000..0629f71 --- /dev/null +++ b/src/photos.test.ts @@ -0,0 +1,129 @@ +import { describe, expect, it, vi } from 'vitest'; +import { PhotoGallery, wrapIndex } from './photos'; +import type { Photo } from './photo-types'; + +const variant = (name: string) => ({ + src: `static/photos/${name}.jpg`, + width: 320, + height: 240, +}); + +const photo = (id: string, alt: string): Photo => ({ + id, + file: `${id}.jpg`, + order: 1, + alt, + caption: alt, + orientation: 'landscape', + width: 320, + height: 240, + aspectRatio: 1.3333, + sources: [ + { + type: 'image/avif', + variants: [variant(`${id}.avif`)], + srcset: `static/photos/${id}.avif 320w`, + }, + { + type: 'image/webp', + variants: [variant(`${id}.webp`)], + srcset: `static/photos/${id}.webp 320w`, + }, + { + type: 'image/jpeg', + variants: [variant(`${id}.jpg`)], + srcset: `static/photos/${id}.jpg 320w`, + }, + ], + fallback: variant(`${id}.jpg`), +}); + +const setup = () => { + vi.stubGlobal('CSS', { + escape: (value: string) => value, + }); + vi.stubGlobal('matchMedia', () => ({ + matches: false, + media: '(prefers-reduced-motion: reduce)', + onchange: null, + addEventListener: vi.fn(), + removeEventListener: vi.fn(), + addListener: vi.fn(), + removeListener: vi.fn(), + dispatchEvent: vi.fn(), + })); + window.HTMLElement.prototype.scrollIntoView = vi.fn(); + + document.body.innerHTML = ` +
+ One + Two +
+
+ + `; + + const gallery = document.querySelector('#gallery'); + const frame = document.querySelector('#frame'); + const toggle = document.querySelector('#toggle'); + + if (!gallery || !frame || !toggle) { + throw new Error('Test DOM failed to initialize.'); + } + + return { + gallery, + frame, + toggle, + photos: [photo('one', 'First photo'), photo('two', 'Second photo')], + }; +}; + +describe('wrapIndex', () => { + it('wraps backward to the last item', () => { + expect(wrapIndex(-1, 4)).toBe(3); + }); + + it('wraps forward to the first item', () => { + expect(wrapIndex(4, 4)).toBe(0); + }); + + it('rejects empty collections', () => { + expect(() => wrapIndex(0, 0)).toThrow('without items'); + }); +}); + +describe('PhotoGallery', () => { + it('updates the frame and selected state when a thumbnail is clicked', () => { + const { gallery, frame, toggle, photos } = setup(); + new PhotoGallery({ photos, gallery, frame, toggle, autoAdvance: false }); + + gallery + .querySelector('[data-photo-id="two"]') + ?.dispatchEvent( + new MouseEvent('click', { bubbles: true, cancelable: true }) + ); + + expect(frame.querySelector('img')?.alt).toBe('Second photo'); + expect( + gallery + .querySelector('[data-photo-id="two"]') + ?.getAttribute('aria-current') + ).toBe('true'); + }); + + it('uses scoped arrow-key navigation', () => { + const { gallery, frame, toggle, photos } = setup(); + new PhotoGallery({ photos, gallery, frame, toggle, autoAdvance: false }); + + gallery.dispatchEvent( + new KeyboardEvent('keydown', { + key: 'ArrowLeft', + bubbles: true, + cancelable: true, + }) + ); + + expect(frame.querySelector('img')?.alt).toBe('Second photo'); + }); +}); diff --git a/src/photos.ts b/src/photos.ts index 7493b39..6c575b1 100644 --- a/src/photos.ts +++ b/src/photos.ts @@ -1,109 +1,273 @@ -import { ResponsiveImage } from './model/responsive-image'; -import { createImage } from './helper/create-image'; +import type { ImageSource, Photo } from './photo-types'; -export class Photos { - private readonly images: Array; - private _selectedId: number; - private timerId: NodeJS.Timeout = null; - private shouldNotChange = false; +const AUTO_ADVANCE_DELAY_MS = 7000; +const FRAME_SIZES = + '(max-width: 899px) calc(100vw - 2rem), calc(100vw - 18rem)'; - public constructor( - images: Array, - private readonly landscapesContainer: HTMLElement, - private readonly portraitsContainer: HTMLElement, - private readonly pictureFrame: HTMLImageElement - ) { - this.images = this.convertImages(images); - this.placeImages(); - this.addEventListeners(); - this.startAutoChange(); - this.selectedId = 0; +type GalleryOptions = { + readonly photos: readonly Photo[]; + readonly gallery: HTMLElement; + readonly frame: HTMLElement; + readonly toggle: HTMLButtonElement; + readonly autoAdvance?: boolean; +}; + +type SelectOptions = { + readonly focusThumbnail?: boolean; + readonly pause?: boolean; +}; + +export const requiredElement = ( + selector: string, + constructor: { new (...args: never[]): T } +): T => { + const element = document.querySelector(selector); + + if (!(element instanceof constructor)) { + throw new Error(`Required element not found: ${selector}`); } - private get selectedId(): number { - return this._selectedId; + return element; +}; + +export const wrapIndex = (value: number, count: number): number => { + if (count <= 0) { + throw new Error('Cannot wrap an index without items.'); } - private set selectedId(value: number) { - if (value < 0) { - value += this.images.length - 1; + return ((value % count) + count) % count; +}; + +const createSource = (source: ImageSource): HTMLSourceElement => { + const element = document.createElement('source'); + element.type = source.type; + element.srcset = source.srcset; + element.sizes = FRAME_SIZES; + + return element; +}; + +const createFrameFigure = (photo: Photo): HTMLElement => { + const figure = document.createElement('figure'); + figure.className = 'frame-figure'; + + const picture = document.createElement('picture'); + + for (const source of photo.sources.filter( + ({ type }) => type !== 'image/jpeg' + )) { + picture.append(createSource(source)); + } + + const jpeg = photo.sources.find(({ type }) => type === 'image/jpeg'); + const image = document.createElement('img'); + image.id = 'frame-image'; + image.src = photo.fallback.src; + image.srcset = jpeg?.srcset ?? ''; + image.sizes = FRAME_SIZES; + image.width = photo.width; + image.height = photo.height; + image.alt = photo.alt; + image.decoding = 'async'; + image.fetchPriority = 'high'; + + picture.append(image); + + const caption = document.createElement('figcaption'); + caption.id = 'frame-caption'; + caption.textContent = photo.caption; + + figure.append(picture, caption); + + return figure; +}; + +export class PhotoGallery { + private selectedIndex = 0; + private timerId: number | null = null; + private readonly thumbnails: HTMLAnchorElement[]; + private readonly reducedMotion = window.matchMedia( + '(prefers-reduced-motion: reduce)' + ); + + public constructor(private readonly options: GalleryOptions) { + if (options.photos.length === 0) { + throw new Error('The gallery needs at least one photo.'); } - this._selectedId = value % this.images.length; - this.loadToFrame(this.images[this.selectedId]); - this.images[this.selectedId].focus(); - this.shouldNotChange = true; - } - private convertImages(images: Array) { - let portraits = 0; // evens - let landscapes = 1; // odds - - const imgs = images.map((image, i) => - createImage( - image, - 'a photo', - Photos.isLandscape(image) ? (landscapes += 2) : (portraits += 2) - ) + this.thumbnails = this.options.photos.map((photo) => + this.thumbnailFor(photo) ); + this.addEventListeners(); + this.select(0); - return imgs.sort((a, b) => a.tabIndex - b.tabIndex); + if (this.options.autoAdvance !== false && !this.reducedMotion.matches) { + this.startAutoAdvance(); + } else { + this.updateToggle(false); + } } - private placeImages() { - this.images.forEach(img => { - if (Photos.isLandscape(img)) { - img.classList.add('landscape'); - this.landscapesContainer.appendChild(img); - } else { - img.classList.add('portrait'); - this.portraitsContainer.appendChild(img); + private addEventListeners(): void { + this.options.gallery.addEventListener('click', (event) => { + const thumbnail = (event.target as Element).closest( + '[data-photo-id]' + ); + + if (!thumbnail) { + return; } - }); - } - private addEventListeners() { - window.document.addEventListener('keydown', e => { - switch (e.key) { + event.preventDefault(); + this.selectById(thumbnail.dataset.photoId, { pause: true }); + }); + + this.options.gallery.addEventListener('keydown', (event) => { + if (!this.options.gallery.contains(event.target as Node)) { + return; + } + + switch (event.key) { case 'ArrowLeft': - this.selectedId--; + event.preventDefault(); + this.select(this.selectedIndex - 1, { + focusThumbnail: true, + pause: true, + }); break; case 'ArrowRight': - this.selectedId++; + event.preventDefault(); + this.select(this.selectedIndex + 1, { + focusThumbnail: true, + pause: true, + }); + break; + case 'Home': + event.preventDefault(); + this.select(0, { focusThumbnail: true, pause: true }); + break; + case 'End': + event.preventDefault(); + this.select(this.options.photos.length - 1, { + focusThumbnail: true, + pause: true, + }); break; - case 'Tab': - if (this.timerId !== null) { - clearInterval(this.timerId); - this.timerId = null; - } } }); - window.document.addEventListener( - 'scroll', - () => (this.shouldNotChange = true) + this.options.gallery.addEventListener('pointerdown', () => + this.pauseAutoAdvance() + ); + this.options.gallery.addEventListener( + 'wheel', + () => this.pauseAutoAdvance(), + { passive: true } + ); + this.options.gallery.addEventListener( + 'touchstart', + () => this.pauseAutoAdvance(), + { passive: true } ); - this.images.forEach((image, i) => - image.addEventListener('click', () => (this.selectedId = i)) - ); - } - - private startAutoChange() { - this.timerId = setInterval(() => { - if (!this.shouldNotChange) { - this.selectedId++; + this.options.toggle.addEventListener('click', () => { + if (this.timerId === null) { + this.startAutoAdvance(); + } else { + this.pauseAutoAdvance(); } - this.shouldNotChange = false; - }, 7000); + }); + + this.reducedMotion.addEventListener('change', () => { + if (this.reducedMotion.matches) { + this.pauseAutoAdvance(); + } + }); } - private loadToFrame(img: HTMLImageElement) { - this.pictureFrame.src = img.src; + private thumbnailFor(photo: Photo): HTMLAnchorElement { + const thumbnail = this.options.gallery.querySelector( + `[data-photo-id="${CSS.escape(photo.id)}"]` + ); + + if (!thumbnail) { + throw new Error(`Missing thumbnail for ${photo.id}.`); + } + + return thumbnail; } - private static isLandscape( - image: HTMLImageElement | ResponsiveImage - ): boolean { - return image.width > image.height; + private selectById(id: string | undefined, options: SelectOptions): void { + const index = this.options.photos.findIndex((photo) => photo.id === id); + + if (index >= 0) { + this.select(index, options); + } + } + + private select(value: number, options: SelectOptions = {}): void { + this.selectedIndex = wrapIndex(value, this.options.photos.length); + const photo = this.options.photos[this.selectedIndex]; + + if (!photo) { + throw new Error('Selected photo is missing.'); + } + + this.options.frame.replaceChildren(createFrameFigure(photo)); + this.updateSelectedThumbnail(photo, options.focusThumbnail ?? false); + + if (options.pause) { + this.pauseAutoAdvance(); + } + } + + private updateSelectedThumbnail(photo: Photo, shouldFocus: boolean): void { + for (const thumbnail of this.thumbnails) { + const selected = thumbnail.dataset.photoId === photo.id; + if (selected) { + thumbnail.setAttribute('aria-current', 'true'); + } else { + thumbnail.removeAttribute('aria-current'); + } + thumbnail.classList.toggle('is-selected', selected); + + if (selected && shouldFocus) { + thumbnail.focus({ preventScroll: true }); + thumbnail.scrollIntoView({ + block: 'nearest', + inline: 'nearest', + }); + } + } + } + + private startAutoAdvance(): void { + this.pauseAutoAdvance(false); + + if (this.reducedMotion.matches) { + this.updateToggle(false); + return; + } + + this.timerId = window.setInterval(() => { + this.select(this.selectedIndex + 1); + }, AUTO_ADVANCE_DELAY_MS); + this.updateToggle(true); + } + + private pauseAutoAdvance(updateToggle = true): void { + if (this.timerId !== null) { + window.clearInterval(this.timerId); + this.timerId = null; + } + + if (updateToggle) { + this.updateToggle(false); + } + } + + private updateToggle(isRunning: boolean): void { + this.options.toggle.setAttribute('aria-pressed', String(isRunning)); + this.options.toggle.textContent = isRunning ? 'Pause' : 'Play'; } } diff --git a/stylelint.config.js b/stylelint.config.js new file mode 100644 index 0000000..d14edcb --- /dev/null +++ b/stylelint.config.js @@ -0,0 +1,9 @@ +export default { + extends: ['stylelint-config-standard-scss'], + rules: { + 'selector-id-pattern': null, + 'selector-class-pattern': + '^[a-z][a-z0-9]*(?:-[a-z0-9]+)*(?:--[a-z0-9]+(?:-[a-z0-9]+)*)?$', + 'scss/dollar-variable-pattern': null, + }, +}; diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..fa7e277 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + environment: 'jsdom', + globals: true, + include: ['src/**/*.test.ts'], + }, +}); diff --git a/webpack.config.js b/webpack.config.js deleted file mode 100644 index 2d527fc..0000000 --- a/webpack.config.js +++ /dev/null @@ -1,174 +0,0 @@ -const path = require('path'); -const HtmlWebpackPlugin = require('html-webpack-plugin'); -const { CleanWebpackPlugin } = require('clean-webpack-plugin'); -const TerserJSPlugin = require('terser-webpack-plugin'); -const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin'); -const MiniCssExtractPlugin = require('mini-css-extract-plugin'); -const HtmlWebpackInlineSourcePlugin = require('html-webpack-inline-source-plugin'); -const Sharp = require('responsive-loader/sharp'); -const Sass = require('sass'); - -const isProduction = process.env.NODE_ENV === 'production'; - -module.exports = { - watchOptions: { - ignored: /node_modules/, - }, - devServer: { - host: '0.0.0.0', - disableHostCheck: true, - }, - optimization: { - minimizer: [ - new TerserJSPlugin({ - sourceMap: !isProduction, - }), - new OptimizeCSSAssetsPlugin({}), - ], - }, - plugins: [ - new CleanWebpackPlugin(), - new HtmlWebpackPlugin({ - xhtml: true, - template: './src/index.html', - minify: { - collapseWhitespace: true, - removeComments: true, - removeRedundantAttributes: true, - removeScriptTypeAttributes: true, - removeStyleLinkTypeAttributes: true, - useShortDoctype: true, - }, - inlineSource: '.(js|css)$', - }), - new HtmlWebpackInlineSourcePlugin(), - new MiniCssExtractPlugin({ - filename: '[name].[contenthash].css', - chunkFilename: '[id].[contenthash].css', - }), - ], - entry: { - index: './src/index.ts', - }, - module: { - rules: [ - { - test: /\.(jpe?g|png)$/i, - loader: 'responsive-loader', - options: { - adapter: Sharp, - quality: 90, - outputPath: 'static/', - sizes: [300, 800, 1200, 2000], - placeholder: false, - }, - }, - { - test: /\.(webm|mp4|gif)$/i, - use: [ - { - loader: 'file-loader', - query: { - outputPath: 'static/', - }, - }, - { - loader: 'image-webpack-loader', - options: { - disable: !isProduction, - mozjpeg: { - progressive: true, - quality: 65, - }, - optipng: { - enabled: true, - }, - pngquant: { - quality: [0.65, 0.9], - speed: 4, - }, - gifsicle: { - interlaced: false, - }, - webp: { - quality: 65, - }, - }, - }, - ], - }, - { - test: /\.svg$/, - loader: 'svg-url-loader', - options: { - limit: 10 * 1024, - noquotes: true, - }, - }, - { - test: /\.(pdf)$/i, - use: { - loader: 'file-loader', - query: { - outputPath: 'static/', - name: '[name].[ext]', - }, - }, - }, - { - test: /no-change.*(ico|html|txt)$/i, - use: { - loader: 'file-loader', - query: { - outputPath: '/', - name: '[name].[ext]', - }, - }, - }, - { - test: /\.scss$/i, - use: [ - MiniCssExtractPlugin.loader, - 'css-loader', - 'postcss-loader', - { - loader: 'resolve-url-loader', - options: { - keepQuery: true, - }, - }, - { - loader: 'sass-loader', - options: { - sourceMap: true, - implementation: Sass, - }, - }, - ], - }, - { - test: /\.(woff2?|ttf|eot|svg)(?:[?#].+)?$/, - use: { - loader: 'file-loader', - options: { - name: '[name].[ext]', - outputPath: 'static/fonts/', - }, - }, - include: /fonts/, - }, - { - test: /\.ts$/, - use: 'ts-loader', - exclude: /node_modules/, - }, - ], - }, - resolve: { - extensions: ['.ts', '.js'], - }, - output: { - filename: '[name].[contenthash].js', - path: path.resolve(__dirname, 'dist'), - }, -};