Compare commits

...

3 commits

Author SHA1 Message Date
isUnknown
d42217fd20 Refactor: config Kirby scindée en menu.php + thumbs.php
All checks were successful
Deploy / Deploy to Production (push) Successful in 17s
- menu.php: helper menuItem() — détection active mutualisée
- thumbs.php: helpers srcsetPreset/srcsetPair — widths définis une seule fois,
  paires normal/webp générées automatiquement sans duplication
- config.php: réduit à l'essentiel via require

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-10 08:21:06 +01:00
isUnknown
0ea4bfe539 Feat: images responsives mockup Portfolio via ResponsivePicture
- Config: presets mockup + mockup-webp (350/480/700/960w)
- portfolio.json.php: expose mockupSrcset + mockupWebp
- Portfolio.svelte: remplace <img> par ResponsivePicture
  sizes="(max-width: 700px) 90vw, 25vw"

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-10 08:12:20 +01:00
isUnknown
11a2c623cb Feat: images responsives vignettes Play + composant ResponsivePicture
- Config: presets thumbnail + thumbnail-webp (170/255/355/510/710w)
- play.json.php: expose thumbnailSrcset + thumbnailWebp
- Nouveau composant ResponsivePicture.svelte (src, srcset, webp, sizes, alt, cls)
- Play.svelte: utilise ResponsivePicture dans le carousel
  sizes="clamp(170px, 18.41vw, 355px)"

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-10 08:08:34 +01:00
8 changed files with 153 additions and 112 deletions

View file

@ -1,113 +1,14 @@
<?php
return [
// Debug mode (désactiver en production)
'debug' => true,
// Panel configuration
'panel' => [
'css' => 'assets/css/custom-panel.css',
'theme' => 'dark',
// Menu personnalisé du panel
'menu' => [
'site' => [
'label' => 'Données partagées',
'icon' => 'cog',
'current' => function (string $current): bool {
$path = Kirby\Cms\App::instance()->path();
return Str::contains($path, '/site');
}
],
'-', // Séparateur
'home' => [
'label' => 'Accueil',
'icon' => 'home',
'link' => 'pages/home',
'current' => function (string $current): bool {
$path = Kirby\Cms\App::instance()->path();
return Str::contains($path, 'pages/home');
}
],
'expertise' => [
'label' => 'Expertise',
'icon' => 'wand',
'link' => 'pages/expertise',
'current' => function (string $current): bool {
$path = Kirby\Cms\App::instance()->path();
return Str::contains($path, 'pages/expertise');
}
],
'portfolio' => [
'label' => 'Portfolio',
'icon' => 'images',
'link' => 'pages/portfolio',
'current' => function (string $current): bool {
$path = Kirby\Cms\App::instance()->path();
return Str::contains($path, 'pages/portfolio');
}
],
'jouer' => [
'label' => 'Jouer',
'icon' => 'play',
'link' => 'pages/jouer',
'current' => function (string $current): bool {
$path = Kirby\Cms\App::instance()->path();
return Str::contains($path, 'pages/jouer');
}
],
'a-propos' => [
'label' => 'À propos',
'icon' => 'users',
'link' => 'pages/a-propos',
'current' => function (string $current): bool {
$path = Kirby\Cms\App::instance()->path();
return Str::contains($path, 'pages/a-propos');
}
],
'blog' => [
'label' => 'Blog',
'icon' => 'text',
'link' => 'pages/blog',
'current' => function (string $current): bool {
$path = Kirby\Cms\App::instance()->path();
return Str::contains($path, 'pages/blog');
}
],
'-', // Séparateur
'users',
'system'
]
],
// Langues
'debug' => true,
'languages' => true,
// Thumbs
'thumbs' => [
'quality' => 80,
'srcsets' => [
'default' => [
'300w' => ['width' => 300],
'600w' => ['width' => 600],
'900w' => ['width' => 900],
'1200w' => ['width' => 1200],
],
// Galerie portfolio — desktop ~15vw, mobile ~33vw (3 colonnes)
// Widths couvrent 1x et 2x retina pour les deux breakpoints
'gallery' => [
'200w' => ['width' => 200],
'300w' => ['width' => 300],
'400w' => ['width' => 400],
'600w' => ['width' => 600],
'800w' => ['width' => 800],
],
'gallery-webp' => [
'200w' => ['width' => 200, 'format' => 'webp'],
'300w' => ['width' => 300, 'format' => 'webp'],
'400w' => ['width' => 400, 'format' => 'webp'],
'600w' => ['width' => 600, 'format' => 'webp'],
'800w' => ['width' => 800, 'format' => 'webp'],
],
],
'panel' => [
'css' => 'assets/css/custom-panel.css',
'theme' => 'dark',
'menu' => require __DIR__ . '/menu.php',
],
];
'thumbs' => require __DIR__ . '/thumbs.php',
];

36
site/config/menu.php Normal file
View file

@ -0,0 +1,36 @@
<?php
/**
* Helper : génère un item de menu avec détection automatique du lien actif.
*/
function menuItem(string $id, string $label, string $icon, string $link): array
{
return [
'label' => $label,
'icon' => $icon,
'link' => $link,
'current' => function () use ($link): bool {
return Str::contains(Kirby\Cms\App::instance()->path(), $link);
},
];
}
return [
'site' => [
'label' => 'Données partagées',
'icon' => 'cog',
'current' => function (): bool {
return Str::contains(Kirby\Cms\App::instance()->path(), '/site');
},
],
'-',
'home' => menuItem('home', 'Accueil', 'home', 'pages/home'),
'expertise' => menuItem('expertise', 'Expertise','wand', 'pages/expertise'),
'portfolio' => menuItem('portfolio', 'Portfolio','images', 'pages/portfolio'),
'jouer' => menuItem('jouer', 'Jouer', 'play', 'pages/jouer'),
'a-propos' => menuItem('a-propos', 'À propos', 'users', 'pages/a-propos'),
'blog' => menuItem('blog', 'Blog', 'text', 'pages/blog'),
'-',
'users',
'system',
];

54
site/config/thumbs.php Normal file
View file

@ -0,0 +1,54 @@
<?php
/**
* Génère un preset srcset Kirby à partir d'un tableau [label => largeur_px].
* Applique le multiplicateur et optionnellement un format (ex: 'webp').
*/
function srcsetPreset(array $widths, float $mult, ?string $format = null): array
{
$preset = [];
foreach ($widths as $label => $px) {
$entry = ['width' => (int) round($px * $mult)];
if ($format !== null) {
$entry['format'] = $format;
}
$preset[$label] = $entry;
}
return $preset;
}
/**
* Génère une paire de presets (original + webp) depuis un tableau de widths.
* Retourne ['name' => [...], 'name-webp' => [...]].
*/
function srcsetPair(string $name, array $widths, float $mult): array
{
return [
$name => srcsetPreset($widths, $mult),
$name . '-webp' => srcsetPreset($widths, $mult, 'webp'),
];
}
// Multiplicateur global (ex: 1.5 pour avoir de la marge sur les écrans Retina)
$m = 1.5;
// Définition des widths par usage (une seule fois)
$widths = [
'default' => ['300w' => 300, '600w' => 600, '900w' => 900, '1200w' => 1200],
// Galerie portfolio — desktop ~15vw, mobile ~33vw (3 colonnes)
'gallery' => ['200w' => 200, '300w' => 300, '400w' => 400, '600w' => 600, '800w' => 800],
// Mockup portfolio — desktop ~25vw, mobile ~90vw
'mockup' => ['350w' => 350, '480w' => 480, '700w' => 700, '960w' => 960],
// Vignettes jeux — active: clamp(170px, 18.41vw, 355px)
'thumbnail' => ['170w' => 170, '255w' => 255, '355w' => 355, '510w' => 510, '710w' => 710],
];
return [
'quality' => 80,
'srcsets' => array_merge(
['default' => srcsetPreset($widths['default'], $m)],
srcsetPair('gallery', $widths['gallery'], $m),
srcsetPair('mockup', $widths['mockup'], $m),
srcsetPair('thumbnail', $widths['thumbnail'], $m),
),
];

View file

@ -8,6 +8,8 @@ $specificData = [
'lettering' => $game->lettering()->toFile()?->url(),
'description' => $game->description()->value(),
'thumbnail' => $game->thumbnail()->toFile()?->url(),
'thumbnailSrcset' => $game->thumbnail()->toFile()?->srcset('thumbnail'),
'thumbnailWebp' => $game->thumbnail()->toFile()?->srcset('thumbnail-webp'),
'backgroundColor' => $game->backgroundColor()->value() ?: null,
'preview' => $game->preview()->toFile()?->url(),
'playLink' => $game->playLink()->value() ?: null,

View file

@ -15,6 +15,8 @@ $specificData = [
'webp' => $f->srcset('gallery-webp'),
])->values(),
'mockup' => $project->mockup()->toFile()?->url(),
'mockupSrcset' => $project->mockup()->toFile()?->srcset('mockup'),
'mockupWebp' => $project->mockup()->toFile()?->srcset('mockup-webp'),
'galleryBackgroundColor' => $project->galleryBackgroundColor()->value(),
'keywords' => $project->keywords()->toStructure()->map(fn($i) => [
'label' => $i->label()->value(),

View file

@ -0,0 +1,28 @@
<script>
/**
* Image responsive avec support WebP via <picture>.
*
* @prop {string} src — URL fallback (format original)
* @prop {string} srcset — srcset format original (ex: "200w ..., 400w ...")
* @prop {string} webp — srcset format WebP
* @prop {string} sizes — valeur de l'attribut sizes
* @prop {string} [alt] — texte alternatif
* @prop {string} [cls] — classe CSS à appliquer sur <img>
*/
let { src, srcset, webp, sizes, alt = '', cls = '' } = $props()
</script>
<picture>
{#if webp}
<source type="image/webp" srcset={webp} {sizes} />
{/if}
<img
{src}
{srcset}
{sizes}
{alt}
class={cls}
loading="lazy"
decoding="async"
/>
</picture>

View file

@ -1,6 +1,7 @@
<script>
import { onMount } from 'svelte'
import { slides } from '@state/slides.svelte'
import ResponsivePicture from '@components/ui/ResponsivePicture.svelte'
let { data } = $props()
@ -196,7 +197,13 @@
onclick={() => selectGame(i)}
>
{#if game.thumbnail}
<img src={game.thumbnail} alt="" />
<ResponsivePicture
src={game.thumbnail}
srcset={game.thumbnailSrcset}
webp={game.thumbnailWebp}
sizes="clamp(170px, 18.41vw, 355px)"
alt=""
/>
{/if}
<span class="play-carousel-title">{game.title}</span>
</button>
@ -360,7 +367,8 @@
width: clamp(170px, 18.41vw, 355px);
}
.play-carousel-item button img {
.play-carousel-item button img,
.play-carousel-item button :global(picture img) {
object-fit: cover;
transition: border-color 0.4s var(--ease-standard);
}
@ -371,7 +379,8 @@
text-align: center;
}
.play-carousel-item.active button img {
.play-carousel-item.active button img,
.play-carousel-item.active button :global(picture img) {
border: 4px solid var(--color-primary);
border-radius: 25%;
}

View file

@ -3,6 +3,7 @@
import { slides } from '@state/slides.svelte'
import { createScrollNav } from '@composables/useScrollNav.svelte.js'
import GalleryAnimation from '@components/ui/GalleryAnimation.svelte'
import ResponsivePicture from '@components/ui/ResponsivePicture.svelte'
let { data } = $props()
@ -101,7 +102,14 @@
<!-- Mockup device (centre) -->
<div class="portfolio-mockup">
<img src={currentProject.mockup} alt={currentProject.title} />
<ResponsivePicture
src={currentProject.mockup}
srcset={currentProject.mockupSrcset}
webp={currentProject.mockupWebp}
sizes="(max-width: 700px) 90vw, 25vw"
alt={currentProject.title}
cls="portfolio-mockup-img"
/>
</div>
<!-- Infos projet (droite) -->
@ -175,7 +183,8 @@
justify-content: center;
}
.portfolio-mockup img {
.portfolio-mockup :global(picture),
.portfolio-mockup :global(.portfolio-mockup-img) {
width: 100%;
height: 100%;
object-fit: contain;