Compare commits

...

4 commits

Author SHA1 Message Date
isUnknown
34635f7982 Fix: navigation clavier Play — capture phase + stopImmediatePropagation
All checks were successful
Deploy / Deploy to Production (push) Successful in 18s
- Handler enregistré en { capture: true } → s'exécute avant App.svelte
- stopImmediatePropagation quand navigation interne (pas aux limites)
  → empêche App de changer de slide en même temps
- Aux limites : aucune interception → App gère naturellement la slide adjacente
- Supprimé : slideTo import, prevSlidePath/nextSlidePath (inutiles)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-10 09:18:44 +01:00
isUnknown
fdab621b48 Feat: navigation clavier + routing ancre sur Play
- ArrowRight/Left : navigue entre les jeux
  aux limites (premier/dernier), passe à la slide prev/next
- Ancres URL (#slug) : set à chaque changement de jeu,
  restaurées au chargement, effacées quand on quitte la slide

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-10 09:03:10 +01:00
isUnknown
90f155b679 Fix: blueprint game + styles carrousel Play
- playLink renommé et translate: false sur playLink + backgroundColor
- button { border: none } dans buttons.css
- Largeur vignettes via :global(img) sur l'élément image
  (découple la taille du bouton de celle de l'image)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-10 09:01:58 +01:00
isUnknown
66767f0136 Feat: breakpoints CSS en @custom-media via postcss-custom-media
- postcss.config.js: plugin postcss-custom-media
- variables.css: @custom-media --mobile / --tablet / --tablet-only
- Remplacement de tous les max-width: 700px et 912px hardcodés
  par @media (--mobile), (--tablet), (--tablet-only)

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

130
package-lock.json generated
View file

@ -16,7 +16,100 @@
"vite": "7.0.4"
},
"devDependencies": {
"fs-extra": "^11.3.3"
"fs-extra": "^11.3.3",
"postcss": "^8.5.8",
"postcss-custom-media": "^12.0.1"
}
},
"node_modules/@csstools/cascade-layer-name-parser": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@csstools/cascade-layer-name-parser/-/cascade-layer-name-parser-3.0.0.tgz",
"integrity": "sha512-/3iksyevwRfSJx5yH0RkcrcYXwuhMQx3Juqf40t97PeEy2/Mz2TItZ/z/216qpe4GgOyFBP8MKIwVvytzHmfIQ==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/csstools"
},
{
"type": "opencollective",
"url": "https://opencollective.com/csstools"
}
],
"license": "MIT",
"engines": {
"node": ">=20.19.0"
},
"peerDependencies": {
"@csstools/css-parser-algorithms": "^4.0.0",
"@csstools/css-tokenizer": "^4.0.0"
}
},
"node_modules/@csstools/css-parser-algorithms": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz",
"integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/csstools"
},
{
"type": "opencollective",
"url": "https://opencollective.com/csstools"
}
],
"license": "MIT",
"engines": {
"node": ">=20.19.0"
},
"peerDependencies": {
"@csstools/css-tokenizer": "^4.0.0"
}
},
"node_modules/@csstools/css-tokenizer": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz",
"integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/csstools"
},
{
"type": "opencollective",
"url": "https://opencollective.com/csstools"
}
],
"license": "MIT",
"engines": {
"node": ">=20.19.0"
}
},
"node_modules/@csstools/media-query-list-parser": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/@csstools/media-query-list-parser/-/media-query-list-parser-5.0.0.tgz",
"integrity": "sha512-T9lXmZOfnam3eMERPsszjY5NK0jX8RmThmmm99FZ8b7z8yMaFZWKwLWGZuTwdO3ddRY5fy13GmmEYZXB4I98Eg==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/csstools"
},
{
"type": "opencollective",
"url": "https://opencollective.com/csstools"
}
],
"license": "MIT",
"engines": {
"node": ">=20.19.0"
},
"peerDependencies": {
"@csstools/css-parser-algorithms": "^4.0.0",
"@csstools/css-tokenizer": "^4.0.0"
}
},
"node_modules/@esbuild/aix-ppc64": {
@ -1161,9 +1254,9 @@
}
},
"node_modules/postcss": {
"version": "8.5.6",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
"version": "8.5.8",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz",
"integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==",
"funding": [
{
"type": "opencollective",
@ -1188,6 +1281,35 @@
"node": "^10 || ^12 || >=14"
}
},
"node_modules/postcss-custom-media": {
"version": "12.0.1",
"resolved": "https://registry.npmjs.org/postcss-custom-media/-/postcss-custom-media-12.0.1.tgz",
"integrity": "sha512-66syE14+VeqkUf0rRX0bvbTCbNRJF132jD+ceo8th1dap2YJEAqpdh5uG98CE3IbgHT7m9XM0GIlOazNWqQdeA==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/csstools"
},
{
"type": "opencollective",
"url": "https://opencollective.com/csstools"
}
],
"license": "MIT",
"dependencies": {
"@csstools/cascade-layer-name-parser": "^3.0.0",
"@csstools/css-parser-algorithms": "^4.0.0",
"@csstools/css-tokenizer": "^4.0.0",
"@csstools/media-query-list-parser": "^5.0.0"
},
"engines": {
"node": ">=20.19.0"
},
"peerDependencies": {
"postcss": "^8.4"
}
},
"node_modules/regexparam": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/regexparam/-/regexparam-1.3.0.tgz",

View file

@ -24,6 +24,8 @@
"vite": "7.0.4"
},
"devDependencies": {
"fs-extra": "^11.3.3"
"fs-extra": "^11.3.3",
"postcss": "^8.5.8",
"postcss-custom-media": "^12.0.1"
}
}

5
postcss.config.js Normal file
View file

@ -0,0 +1,5 @@
import customMedia from 'postcss-custom-media'
export default {
plugins: [customMedia()]
}

View file

@ -55,9 +55,10 @@ columns:
- strike
- clear
- link
playLinks:
playLink:
label: Lien(s) pour jouer
help: Laissez vide pour afficher "à venir / coming soon"
translate: false
type: url
- width: 3/10
@ -71,6 +72,7 @@ columns:
language: css
size: custom-size
default: radial-gradient(circle at 20% 80%, rgb(240, 154, 110) 0%, rgb(233, 101, 122) 100%)
translate: false
help: À remplir avec la valeur de la propriété CSS `background-color` souhaitée
preview:
label: Aperçu

View file

@ -166,7 +166,7 @@
}
/* Mobile — var(--breakpoint-mobile) = 700px */
@media screen and (max-width: 700px) {
@media (--mobile) {
.navbar {
min-height: 8vh;
padding: 10vh 0 1vh;
@ -179,7 +179,7 @@
}
/* Tablet — var(--breakpoint-tablet) = 912px */
@media screen and (min-width: 701px) and (max-width: 912px) {
@media (--tablet-only) {
.navbar-item {
font-size: var(--font-size-paragraph-tablet);
padding: 1vmax 1.8vmax;

View file

@ -244,7 +244,7 @@
}
/* Mobile — var(--breakpoint-mobile) = 700px */
@media screen and (max-width: 700px) {
@media (--mobile) {
.menu-list {
font-size: var(--font-size-subtitle-mobile);
grid-area: 6/4 / span 8 / span 8;
@ -269,7 +269,7 @@
}
/* Tablet — var(--breakpoint-tablet) = 912px */
@media screen and (min-width: 701px) and (max-width: 912px) {
@media (--tablet-only) {
.menu-list {
font-size: var(--font-size-title-section-tablet);
}

View file

@ -1,3 +1,7 @@
button {
border: none;
}
/* Button */
.button {
width: 14vmax;

View file

@ -1,3 +1,8 @@
/* Custom media queries — utilisables dans @media (--mobile) / @media (--tablet) */
@custom-media --mobile (max-width: 700px);
@custom-media --tablet (max-width: 912px);
@custom-media --tablet-only (min-width: 701px) and (max-width: 912px);
/* CSS Variables */
:root {
/* Colors */
@ -6,10 +11,6 @@
--color-background: #000;
--color-text: #fff;
/* BREAKPOINTS (référence non utilisables directement dans @media)
* --breakpoint-mobile : 700px
* --breakpoint-tablet : 912px
*/
--breakpoint-mobile: 700px;
--breakpoint-tablet: 912px;

View file

@ -363,7 +363,7 @@
}
/* Mobile (≤ 700px) */
@media screen and (max-width: 700px) {
@media (--mobile) {
.expertise-title {
grid-area: 5/4 / span 1 / span 7;
font-size: var(--font-size-title-main-mobile);
@ -379,7 +379,7 @@
}
/* Tablet (701912px) */
@media screen and (min-width: 701px) and (max-width: 912px) {
@media (--tablet-only) {
.expertise-title {
grid-area: 5/6 / span 4 / span 12;
font-size: var(--font-size-title-main-tablet);

View file

@ -175,7 +175,7 @@
}
}
@media screen and (min-width: 701px) and (max-width: 912px) {
@media (--tablet-only) {
.home-subtitle {
font-size: var(--font-size-subtitle-tablet);
width: 70%;

View file

@ -10,25 +10,45 @@
const games = $derived(data?.games ?? [])
// --- State ---
let currentIndex = $state(0) // index actif (carousel + bg)
let displayedIndex = $state(0) // index du contenu affiché
let currentIndex = $state(0)
let displayedIndex = $state(0)
let phase = $state('idle') // 'idle' | 'exiting' | 'entering'
let slideDir = $state(1) // 1 = click droite (exit gauche), -1 = click gauche (exit droite)
let slideDir = $state(1) // 1 = exit gauche, -1 = exit droite
// Bg crossfade
let prevBgColor = $state(null)
let bgFading = $state(false)
let prevBgColor = $state(null)
let bgFading = $state(false)
const displayedGame = $derived(games[displayedIndex] ?? null)
// Capture du hash synchrone avant que tout effect puisse le modifier
const initialHash = window.location.hash.slice(1)
let t1 = null
let t2 = null
let t3 = null
// --- Ancres ---
function setAnchor(index) {
const slug = games[index]?.slug
if (!slug) return
history.replaceState(null, '', '#' + slug)
}
function clearAnchor() {
history.replaceState(null, '', window.location.pathname + window.location.search)
}
// Restauration depuis l'ancre URL — une seule fois quand games est prêt
$effect(() => {
if (games.length === 0 || !initialHash) return
const idx = games.findIndex(g => g.slug === initialHash)
if (idx > 0) currentIndex = idx
})
function selectGame(i) {
if (i === currentIndex || phase !== 'idle' || !games.length) return
// slideDir = 1 → clic à droite → content sort par la gauche, entre par la droite
slideDir = i > currentIndex ? 1 : -1
phase = 'exiting'
@ -36,22 +56,36 @@
clearTimeout(t2)
clearTimeout(t3)
// Phase 2 (300ms) : swap contenu + carousel + bg simultanés
t1 = setTimeout(() => {
prevBgColor = displayedGame?.backgroundColor ?? null
displayedIndex = i
currentIndex = i
phase = 'entering'
bgFading = true
setAnchor(i)
// Fin du bg fade
t3 = setTimeout(() => { bgFading = false }, 500)
// Fin de l'entrée
t2 = setTimeout(() => { phase = 'idle' }, 350)
}, 300)
}
// --- Clavier ---
// Enregistré en capture pour s'exécuter avant le handler global de App.svelte.
// Si la navigation reste dans Play (pas aux limites), on bloque App via
// stopImmediatePropagation. Aux limites, on ne fait rien : App navigue normalement.
function onKeyDown(e) {
if (!isActive) return
if (e.key === 'ArrowRight' && currentIndex < games.length - 1) {
e.stopImmediatePropagation()
e.preventDefault()
selectGame(currentIndex + 1)
} else if (e.key === 'ArrowLeft' && currentIndex > 0) {
e.stopImmediatePropagation()
e.preventDefault()
selectGame(currentIndex - 1)
}
}
// Réinitialisation quand on quitte la slide
$effect(() => {
if (!isActive) {
@ -62,13 +96,18 @@
displayedIndex = 0
phase = 'idle'
bgFading = false
clearAnchor()
}
})
onMount(() => () => {
clearTimeout(t1)
clearTimeout(t2)
clearTimeout(t3)
onMount(() => {
window.addEventListener('keydown', onKeyDown, { capture: true })
return () => {
window.removeEventListener('keydown', onKeyDown, { capture: true })
clearTimeout(t1)
clearTimeout(t2)
clearTimeout(t3)
}
})
</script>
@ -354,7 +393,6 @@
display: flex;
flex-direction: column;
align-items: center;
width: clamp(140px, 15.09vw, 291px);
gap: 0.5rem;
background: none;
cursor: pointer;
@ -362,15 +400,22 @@
transition: all 0.4s var(--ease-standard);
}
.play-carousel-item.active button {
opacity: 1;
.play-carousel-item :global(img) {
width: clamp(140px, 15.09vw, 291px);
}
.play-carousel-item.active :global(img) {
width: clamp(170px, 18.41vw, 355px);
}
.play-carousel-item button img,
.play-carousel-item.active button {
opacity: 1;
}
.play-carousel-item button :global(img),
.play-carousel-item button :global(picture img) {
object-fit: cover;
transition: border-color 0.4s var(--ease-standard);
transition: all 0.4s var(--ease-standard);
}
.play-carousel-title {
@ -386,7 +431,7 @@
}
/* --- Mobile (≤ 700px) --- */
@media screen and (max-width: 700px) {
@media (--mobile) {
.game-preview {
display: none;
}

View file

@ -291,7 +291,7 @@
}
/* Mobile (≤ 700px) */
@media screen and (max-width: 700px) {
@media (--mobile) {
.portfolio-gallery {
grid-area: 1/1 / span 20 / span 20;
opacity: 0.3;