closes #22
All checks were successful
Deploy / Deploy to Production (push) Successful in 21s

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
isUnknown 2026-03-21 09:03:11 +01:00
parent 875e4d63f5
commit 4803390a1f
25 changed files with 75 additions and 366 deletions

View file

@ -98,7 +98,7 @@
style="width: {wrapperWidth}; transform: {wrapperTransform}" style="width: {wrapperWidth}; transform: {wrapperTransform}"
> >
{#each slides.all as slide, i} {#each slides.all as slide, i}
<section class="slide" class:active={i === slides.activeIndex} data-slide={slide.id}> <section class="slide" class:active={i === slides.activeIndex} data-slide={slide.id} inert={i !== slides.activeIndex}>
{#if slide.loaded} {#if slide.loaded}
<svelte:component <svelte:component
this={templates[slide.template] ?? Default} this={templates[slide.template] ?? Default}

45
src/styles/a11y.css Normal file
View file

@ -0,0 +1,45 @@
a:focus-visible,
button:focus-visible {
border: none;
outline: 2px solid var(--color-primary);
}
a.navbar-logo:focus-visible {
outline-offset: 4px;
}
.button:focus-visible,
.lang-switcher:focus-visible {
outline: 2px solid #fff;
outline-offset: 4px;
}
.play-carousel-item button:focus-visible {
outline: 4px solid #fff;
outline-offset: 4px;
border-radius: 3.4vw;
}
.play-carousel-item.active button:focus-visible {
outline-offset: 6px;
border-radius: 4.6vw;
}
.pagination-dot:focus-visible {
outline: 1px solid var(--color-primary);
outline-offset: 2px;
border-radius: 1rem;
}
.pagination-dot.active:focus-visible {
outline: 1px solid #fff;
}
.footer-bottom a,
.footer-col a {
outline-offset: 4px;
}
.footer-socials a:focus-visible {
outline: 2px solid #fff;
border-radius: 2px;
}

View file

@ -1,5 +1,6 @@
button { button {
border: none; border: 0px solid transparent;
outline: 0px solid transparent;
} }
button[disabled] { button[disabled] {

View file

@ -8,3 +8,4 @@
@import "./cursor.css"; @import "./cursor.css";
@import "./form.css"; @import "./form.css";
@import "./utils.css"; @import "./utils.css";
@import "./a11y.css";

View file

@ -134,16 +134,18 @@
onclick={prevSlide} onclick={prevSlide}
>← BEFORE</button> >← BEFORE</button>
<div class="pagination-indicator"> <ul class="pagination-indicator">
{#each { length: totalSlides } as _, i} {#each { length: totalSlides } as _, i}
<li>
<button <button
class="pagination-dot" class="pagination-dot"
class:active={i === currentSlide} class:active={i === currentSlide}
aria-label="Slide {i + 1}" aria-label="Slide {i + 1}"
onclick={() => goToSlide(i)} onclick={() => goToSlide(i)}
></button> ></button>
</li>
{/each} {/each}
</div> </ul>
<button <button
class="nav-button" class="nav-button"
@ -326,6 +328,10 @@
opacity: 0.8; opacity: 0.8;
} }
.pagination-indicator li {
list-style: none;
}
/* Nav */ /* Nav */
.nav-buttons { .nav-buttons {
display: flex; display: flex;

View file

@ -135,18 +135,18 @@
{#if featured.intro} {#if featured.intro}
<p class="collection-card-description">{featured.intro}</p> <p class="collection-card-description">{featured.intro}</p>
{/if} {/if}
<a href="/blog/{featured.slug}" class="collection-card-readmore desktop-only"> <a href="/blog/{featured.slug}" class="collection-card-readmore desktop-only" tabindex="-1">
{t('read_article')} <span class="arrow"></span> {t('read_article')} <span class="arrow"></span>
</a> </a>
</div> </div>
{#if featured.cover} {#if featured.cover}
<div class="collection-card-image collection-card-image--featured"> <div class="collection-card-image collection-card-image--featured">
<a href="/blog/{featured.slug}"> <a href="/blog/{featured.slug}" tabindex="-1">
<img src={featured.cover} alt={featured.title} /> <img src={featured.cover} alt={featured.title} />
</a> </a>
</div> </div>
{/if} {/if}
<a href="/blog/{featured.slug}" class="collection-card-readmore mobile-only"> <a href="/blog/{featured.slug}" class="collection-card-readmore mobile-only" tabindex="-1">
{t('read_article')} <span class="arrow"></span> {t('read_article')} <span class="arrow"></span>
</a> </a>
</article> </article>
@ -160,18 +160,18 @@
<h2 class="collection-card-title"> <h2 class="collection-card-title">
<a href="/blog/{article.slug}">{article.title}</a> <a href="/blog/{article.slug}">{article.title}</a>
</h2> </h2>
<a href="/blog/{article.slug}" class="collection-card-readmore desktop-only"> <a href="/blog/{article.slug}" class="collection-card-readmore desktop-only" tabindex="-1">
{t('read_article')} <span class="arrow"></span> {t('read_article')} <span class="arrow"></span>
</a> </a>
</div> </div>
{#if article.cover} {#if article.cover}
<div class="collection-card-image"> <div class="collection-card-image">
<a href="/blog/{article.slug}"> <a href="/blog/{article.slug}" tabindex="-1">
<img src={article.cover} alt={article.title} /> <img src={article.cover} alt={article.title} />
</a> </a>
</div> </div>
{/if} {/if}
<a href="/blog/{article.slug}" class="collection-card-readmore mobile-only"> <a href="/blog/{article.slug}" class="collection-card-readmore mobile-only" tabindex="-1">
{t('read_article')} <span class="arrow"></span> {t('read_article')} <span class="arrow"></span>
</a> </a>
</article> </article>

View file

@ -50,6 +50,7 @@
playsinline playsinline
loop loop
preload="auto" preload="auto"
tabindex="-1"
id="home-video" id="home-video"
class="home-video home-video-desktop" class="home-video home-video-desktop"
poster={data.backgroundVideoPoster ?? undefined} poster={data.backgroundVideoPoster ?? undefined}
@ -61,6 +62,7 @@
playsinline playsinline
loop loop
preload="auto" preload="auto"
tabindex="-1"
id="home-video-mobile" id="home-video-mobile"
class="home-video home-video-mobile" class="home-video home-video-mobile"
poster={data.backgroundVideoMobilePoster ?? undefined} poster={data.backgroundVideoMobilePoster ?? undefined}
@ -81,8 +83,6 @@
<button <button
class="button with-icon earth-icon home-cta" class="button with-icon earth-icon home-cta"
onclick={handleExplore} onclick={handleExplore}
onkeypress={(e) => e.key === 'Enter' && handleExplore()}
tabindex="0"
> >
{data.hero.ctaText} {data.hero.ctaText}
</button> </button>
@ -128,7 +128,7 @@
/* Text overlay */ /* Text overlay */
.home-text { .home-text {
z-index: var(--z-content); z-index: var(--z-content);
grid-area: 9/1 / span 6 / span 20; grid-area: 10/1 / span 6 / span 20;
width: 100%; width: 100%;
justify-self: center; justify-self: center;
margin-top: 6vmax; margin-top: 6vmax;

View file

@ -285,7 +285,6 @@
alt="" alt=""
/> />
{/if} {/if}
<span class="play-carousel-title">{game.title}</span>
</button> </button>
</li> </li>
{/each} {/each}
@ -456,16 +455,10 @@
.play-carousel-item.active button :global(picture img) { .play-carousel-item.active button :global(picture img) {
object-fit: cover; object-fit: cover;
transition: all 0.4s var(--ease-standard); transition: all 0.4s var(--ease-standard);
border: 4px solid var(--color-primary); border: 5px solid var(--color-primary);
border-radius: 25%; border-radius: 25%;
} }
.play-carousel-title {
font-size: var(--font-size-caption);
color: var(--color-text);
text-align: center;
}
/* --- Mobile (≤ 700px) --- */ /* --- Mobile (≤ 700px) --- */
@media (max-width: 700px) { @media (max-width: 700px) {
.game-preview { .game-preview {

View file

@ -134,13 +134,13 @@
{#if item.intro} {#if item.intro}
<p class="collection-card-description">{item.intro}</p> <p class="collection-card-description">{item.intro}</p>
{/if} {/if}
<a href="/{pageUri}/{item.slug}" class="collection-card-readmore"> <a href="/{pageUri}/{item.slug}" class="collection-card-readmore" tabindex="-1">
{t('read_wp')} <span class="arrow"></span> {t('read_wp')} <span class="arrow"></span>
</a> </a>
</div> </div>
{#if item.cover} {#if item.cover}
<div class="collection-card-image"> <div class="collection-card-image">
<a href="/{pageUri}/{item.slug}"> <a href="/{pageUri}/{item.slug}" tabindex="-1">
<img src={item.cover} alt={item.title} /> <img src={item.cover} alt={item.title} />
</a> </a>
</div> </div>

View file

@ -1,53 +0,0 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
This is a **prototype for an animated image gallery component** intended for integration into a Kirby CMS project. The animation displays images in a continuous scrolling pattern across multiple columns/rows.
## Running the Project
This is a PHP project with no build system. To run locally:
```bash
php -S localhost:8000
```
Then open `http://localhost:8000` in a browser.
## Architecture
### Configuration (index.php:1-28)
The animation is configured via PHP variables at the top of `index.php`:
- `$mode`: `'vertical'` or `'horizontal'` - scroll direction
- `$set`: Image set name (e.g., `'LEGACY'`, `'OLLY'`)
- `$secondsPerImage`: Controls animation speed
- `$imagesSets`: Associative array defining image paths per set
### Animation System
**CSS-based infinite scroll** using duplicated images:
- Images are rendered twice in each track (`index.php:63-68`) to create seamless looping
- Animation translates from 0 to -50% (or vice versa), jumping back invisibly when the duplicate starts
- Odd columns scroll down/right; even columns scroll up/left
- Column offsets and delays are calculated based on image count to stagger the animation
**Key CSS classes** (BEM notation):
- `.gallery-animation--vertical` / `.gallery-animation--horizontal`: Mode modifiers
- `.gallery-animation__wrapper`: Flex container for columns
- `.gallery-animation__column`: Individual scrolling lane
- `.gallery-animation__track`: Animated element containing images
- `.gallery-animation__image`: Individual image styling
**CSS custom properties**:
- `--animation-duration`: Total cycle duration (set via PHP)
- `--gap`: Spacing between columns/rows
- `--vertical-wrapper-width`: Container width in vertical mode
### Accessibility
Respects `prefers-reduced-motion` by disabling animations (`style.css:149-153`).
## Language
Code comments and documentation are in French.

View file

@ -1,50 +0,0 @@
# Animation Galerie d'Images
## Fichiers
- `style.css` : CSS de l'animation (à intégrer dans le projet Kirby)
- `index.html` : Page de test
- `images/` : Images de test
## Usage
```html
<div class="gallery-animation gallery-animation--vertical">
<!-- ou gallery-animation--horizontal -->
</div>
```
### Variables CSS
```css
.gallery-animation {
--animation-duration: 30s; /* Durée de l'animation */
--gap: 1rem; /* Espacement entre colonnes/rangées */
}
```
### Structure HTML
**Mode vertical** (3 colonnes) :
```html
<div class="gallery-animation gallery-animation--vertical">
<div class="gallery-animation__wrapper">
<div class="gallery-animation__column">
<div class="gallery-animation__track">
<img src="..." class="gallery-animation__image">
<!-- images dupliquées pour boucle infinie -->
</div>
</div>
<!-- 2 autres colonnes -->
</div>
</div>
```
**Mode horizontal** (3 rangées) : remplacer `--vertical` par `--horizontal` et `__column` par `__row`.
## Comportement
- Colonnes impaires : défilent vers le bas / droite
- Colonnes paires : défilent vers le haut / gauche
- Images dupliquées dans le HTML pour transition fluide
- Décalage automatique entre colonnes/rangées via `animation-delay`

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 818 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 574 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 MiB

View file

@ -1,76 +0,0 @@
<?php
// Configuration
$mode = 'horizontal'; // 'vertical' ou 'horizontal'
$set = 'OLLY'; // 'LEGACY' ou 'OLLY'
$secondsPerImage = 8; // vitesse : secondes pour défiler une image
$imagesSets = [
'LEGACY' => [
'images/LEGACY/apples-averywhere.png',
'images/LEGACY/legacy-menu-pause.png',
'images/LEGACY/legacy-screen-6.png',
'images/LEGACY/legacy-screen-7.png',
'images/LEGACY/run-and-eat-to-survive.png',
'images/LEGACY/you-won_t-live-forever.png',
],
'OLLY' => [
'images/OLLY/lesson_contentblock01.png',
'images/OLLY/lesson_contentblock04.png',
'images/OLLY/lesson_contentblock05.png',
'images/OLLY/lesson_cover01.png',
'images/OLLY/quiz_choosemode.png',
'images/OLLY/quiz_quadmode.png',
],
];
$images = $imagesSets[$set];
$count = count($images);
$duration = $count * $secondsPerImage; // durée calculée selon le nombre d'images
// Décalage par colonne basé sur le nombre d'images
// Colonnes 1 et 3 vont dans la même direction, donc décalées de 1/2
// Colonne 2 va dans l'autre direction, décalée de 1/4
$columns = [
['offset' => 0, 'delay' => 0],
['offset' => (int)($count / 3), 'delay' => $duration / 4],
['offset' => 0, 'delay' => $duration / 2],
];
function getShiftedImages($images, $offset) {
return array_merge(
array_slice($images, $offset),
array_slice($images, 0, $offset)
);
}
?>
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Test Animation Galerie</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="gallery-animation gallery-animation--<?= $mode ?>" style="--animation-duration: <?= $duration ?>s;">
<div class="gallery-animation__wrapper">
<?php foreach ($columns as $col):
$colImages = getShiftedImages($images, $col['offset']);
?>
<div class="gallery-animation__column">
<div class="gallery-animation__track" style="animation-delay: -<?= $col['delay'] ?>s;">
<?php foreach ($colImages as $img): ?>
<img src="<?= $img ?>" alt="" class="gallery-animation__image">
<?php endforeach; ?>
<?php foreach ($colImages as $img): ?>
<img src="<?= $img ?>" alt="" class="gallery-animation__image">
<?php endforeach; ?>
</div>
</div>
<?php endforeach; ?>
</div>
</div>
</body>
</html>

View file

@ -1,158 +0,0 @@
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
min-height: 100vh;
}
.gallery-animation {
--gap: 20px;
--vertical-wrapper-width: 900px;
--shadow-diffusion: 5px;
--gallery-animation-width: 40vw;
--max-portion : 0.7;
position: fixed;
top: 0;
left: 0;
width: var(--gallery-animation-width);
height: 100vh;
overflow: hidden;
}
.gallery-animation__wrapper {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
background-color: green;
}
.gallery-animation__track {
display: flex;
animation-timing-function: linear;
animation-iteration-count: infinite;
}
.gallery-animation__image {
object-fit: cover;
flex-shrink: 0;
filter: drop-shadow(0px 0px var(--shadow-diffusion) rgb(0, 0, 0));
}
/* ==========================================================================
MODE VERTICAL
========================================================================== */
.gallery-animation--vertical .gallery-animation__wrapper{
width: var(--vertical-wrapper-width);
flex-direction: row;
justify-content: center;
padding: 0 var(--gap);
left: 50%;
transform: translateX(-50%);
}
.gallery-animation--vertical .gallery-animation__column {
width: min(100%, calc(var(--gallery-animation-width) * var(--max-portion)));
display: flex;
flex-direction: column;
align-items: stretch;
overflow: hidden;
}
.gallery-animation--vertical .gallery-animation__track {
flex-direction: column;
align-items: stretch;
width: 100%;
padding-inline: calc(var(--gap) / 2);
}
.gallery-animation--vertical .gallery-animation__image {
width: 100%;
height: auto;
/* height: calc(100vh * var(--max-portion)); */ /* */
object-fit: contain;
margin-block: calc(var(--gap) / 2);
}
/* Animations verticales */
.gallery-animation--vertical .gallery-animation__column:nth-child(odd) .gallery-animation__track {
animation: scrollDown var(--animation-duration) linear infinite;
}
.gallery-animation--vertical .gallery-animation__column:nth-child(even) .gallery-animation__track {
animation: scrollUp var(--animation-duration) linear infinite;
}
@keyframes scrollDown {
from { transform: translateY(0); }
to { transform: translateY(-50%); }
}
@keyframes scrollUp {
from { transform: translateY(-50%); }
to { transform: translateY(0); }
}
/* ==========================================================================
MODE HORIZONTAL
========================================================================== */
.gallery-animation--horizontal .gallery-animation__wrapper {
flex-direction: column;
padding: var(--gap) 0;
}
.gallery-animation--horizontal .gallery-animation__column {
flex: 1;
display: flex;
flex-direction: row;
align-items: stretch;
overflow: hidden;
}
.gallery-animation--horizontal .gallery-animation__track {
flex-direction: row;
align-items: stretch;
height: 100%;
padding-block: calc(var(--gap) / 2);
}
.gallery-animation--horizontal .gallery-animation__image {
height: 100%;
width: auto;
/* width: calc(var(--gallery-animation-width) * var(--max-portion)); */
/* width: min(100%, calc(var(--gallery-animation-width) * var(--max-portion))); */
object-fit: contain;
margin-inline: calc(var(--gap) / 2);
}
/* Animations horizontales */
.gallery-animation--horizontal .gallery-animation__column:nth-child(odd) .gallery-animation__track {
animation: scrollRight var(--animation-duration) linear infinite;
}
.gallery-animation--horizontal .gallery-animation__column:nth-child(even) .gallery-animation__track {
animation: scrollLeft var(--animation-duration) linear infinite;
}
@keyframes scrollRight {
from { transform: translateX(-50%); }
to { transform: translateX(0); }
}
@keyframes scrollLeft {
from { transform: translateX(0); }
to { transform: translateX(-50%); }
}
/* ==========================================================================
REDUCED MOTION
========================================================================== */
@media (prefers-reduced-motion: reduce) {
.gallery-animation__track {
animation: none !important;
}
}