closes #22
All checks were successful
Deploy / Deploy to Production (push) Successful in 21s
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
|
@ -98,7 +98,7 @@
|
|||
style="width: {wrapperWidth}; transform: {wrapperTransform}"
|
||||
>
|
||||
{#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}
|
||||
<svelte:component
|
||||
this={templates[slide.template] ?? Default}
|
||||
|
|
|
|||
45
src/styles/a11y.css
Normal 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;
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
button {
|
||||
border: none;
|
||||
border: 0px solid transparent;
|
||||
outline: 0px solid transparent;
|
||||
}
|
||||
|
||||
button[disabled] {
|
||||
|
|
|
|||
|
|
@ -8,3 +8,4 @@
|
|||
@import "./cursor.css";
|
||||
@import "./form.css";
|
||||
@import "./utils.css";
|
||||
@import "./a11y.css";
|
||||
|
|
|
|||
|
|
@ -134,16 +134,18 @@
|
|||
onclick={prevSlide}
|
||||
>← BEFORE</button>
|
||||
|
||||
<div class="pagination-indicator">
|
||||
<ul class="pagination-indicator">
|
||||
{#each { length: totalSlides } as _, i}
|
||||
<button
|
||||
class="pagination-dot"
|
||||
class:active={i === currentSlide}
|
||||
aria-label="Slide {i + 1}"
|
||||
onclick={() => goToSlide(i)}
|
||||
></button>
|
||||
<li>
|
||||
<button
|
||||
class="pagination-dot"
|
||||
class:active={i === currentSlide}
|
||||
aria-label="Slide {i + 1}"
|
||||
onclick={() => goToSlide(i)}
|
||||
></button>
|
||||
</li>
|
||||
{/each}
|
||||
</div>
|
||||
</ul>
|
||||
|
||||
<button
|
||||
class="nav-button"
|
||||
|
|
@ -326,6 +328,10 @@
|
|||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.pagination-indicator li {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
/* Nav */
|
||||
.nav-buttons {
|
||||
display: flex;
|
||||
|
|
|
|||
|
|
@ -135,18 +135,18 @@
|
|||
{#if featured.intro}
|
||||
<p class="collection-card-description">{featured.intro}</p>
|
||||
{/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>
|
||||
</a>
|
||||
</div>
|
||||
{#if featured.cover}
|
||||
<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} />
|
||||
</a>
|
||||
</div>
|
||||
{/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>
|
||||
</a>
|
||||
</article>
|
||||
|
|
@ -160,18 +160,18 @@
|
|||
<h2 class="collection-card-title">
|
||||
<a href="/blog/{article.slug}">{article.title}</a>
|
||||
</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>
|
||||
</a>
|
||||
</div>
|
||||
{#if article.cover}
|
||||
<div class="collection-card-image">
|
||||
<a href="/blog/{article.slug}">
|
||||
<a href="/blog/{article.slug}" tabindex="-1">
|
||||
<img src={article.cover} alt={article.title} />
|
||||
</a>
|
||||
</div>
|
||||
{/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>
|
||||
</a>
|
||||
</article>
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@
|
|||
playsinline
|
||||
loop
|
||||
preload="auto"
|
||||
tabindex="-1"
|
||||
id="home-video"
|
||||
class="home-video home-video-desktop"
|
||||
poster={data.backgroundVideoPoster ?? undefined}
|
||||
|
|
@ -61,6 +62,7 @@
|
|||
playsinline
|
||||
loop
|
||||
preload="auto"
|
||||
tabindex="-1"
|
||||
id="home-video-mobile"
|
||||
class="home-video home-video-mobile"
|
||||
poster={data.backgroundVideoMobilePoster ?? undefined}
|
||||
|
|
@ -81,8 +83,6 @@
|
|||
<button
|
||||
class="button with-icon earth-icon home-cta"
|
||||
onclick={handleExplore}
|
||||
onkeypress={(e) => e.key === 'Enter' && handleExplore()}
|
||||
tabindex="0"
|
||||
>
|
||||
{data.hero.ctaText}
|
||||
</button>
|
||||
|
|
@ -128,7 +128,7 @@
|
|||
/* Text overlay */
|
||||
.home-text {
|
||||
z-index: var(--z-content);
|
||||
grid-area: 9/1 / span 6 / span 20;
|
||||
grid-area: 10/1 / span 6 / span 20;
|
||||
width: 100%;
|
||||
justify-self: center;
|
||||
margin-top: 6vmax;
|
||||
|
|
|
|||
|
|
@ -285,7 +285,6 @@
|
|||
alt=""
|
||||
/>
|
||||
{/if}
|
||||
<span class="play-carousel-title">{game.title}</span>
|
||||
</button>
|
||||
</li>
|
||||
{/each}
|
||||
|
|
@ -456,16 +455,10 @@
|
|||
.play-carousel-item.active button :global(picture img) {
|
||||
object-fit: cover;
|
||||
transition: all 0.4s var(--ease-standard);
|
||||
border: 4px solid var(--color-primary);
|
||||
border: 5px solid var(--color-primary);
|
||||
border-radius: 25%;
|
||||
}
|
||||
|
||||
.play-carousel-title {
|
||||
font-size: var(--font-size-caption);
|
||||
color: var(--color-text);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* --- Mobile (≤ 700px) --- */
|
||||
@media (max-width: 700px) {
|
||||
.game-preview {
|
||||
|
|
|
|||
|
|
@ -134,13 +134,13 @@
|
|||
{#if item.intro}
|
||||
<p class="collection-card-description">{item.intro}</p>
|
||||
{/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>
|
||||
</a>
|
||||
</div>
|
||||
{#if item.cover}
|
||||
<div class="collection-card-image">
|
||||
<a href="/{pageUri}/{item.slug}">
|
||||
<a href="/{pageUri}/{item.slug}" tabindex="-1">
|
||||
<img src={item.cover} alt={item.title} />
|
||||
</a>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
@ -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`
|
||||
|
Before Width: | Height: | Size: 2.5 MiB |
|
Before Width: | Height: | Size: 2.5 MiB |
|
Before Width: | Height: | Size: 2.5 MiB |
|
Before Width: | Height: | Size: 2.4 MiB |
|
Before Width: | Height: | Size: 2.4 MiB |
|
Before Width: | Height: | Size: 2.3 MiB |
|
Before Width: | Height: | Size: 818 KiB |
|
Before Width: | Height: | Size: 4.6 MiB |
|
Before Width: | Height: | Size: 574 KiB |
|
Before Width: | Height: | Size: 7.1 MiB |
|
Before Width: | Height: | Size: 1.4 MiB |
|
Before Width: | Height: | Size: 2.3 MiB |
|
|
@ -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>
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||