Compare commits

...

2 commits

Author SHA1 Message Date
isUnknown
436a4371da Fix: is-animated et ancre URL au chargement
All checks were successful
Deploy / Deploy to Production (push) Successful in 21s
is-animated : double rAF garantit un paint avec le bon translateX
avant d'activer la transition (évite l'animation parasite au load).

Ancre : capture du hash synchrone à la création du composant +
flag wasActive pour que clearAnchor() ne s'exécute pas au montage
quand isActive est encore false (slides non initialisées).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-06 12:23:47 +01:00
isUnknown
37b6ca7a5f Feat: ancres URL par projet dans la page Portfolio
- Navigation scroll/clavier → history.replaceState('#slug')
- Clic sidebar → idem
- Chargement avec #slug dans l'URL → affiche le bon projet directement
- Quitter la slide Portfolio → efface le hash

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-06 12:17:58 +01:00
2 changed files with 37 additions and 7 deletions

View file

@ -38,10 +38,11 @@
let resizeTimer = null
// Active la transition seulement après le premier paint à la bonne position.
// Sans ça, un chargement sur /expertise slide depuis l'accueil.
// Double rAF : le premier laisse passer un paint avec le bon translateX,
// le second active is-animated — évite l'animation parasite au chargement.
$effect(() => {
if (slides.all.length > 0 && !isReady) {
requestAnimationFrame(() => { isReady = true })
requestAnimationFrame(() => requestAnimationFrame(() => { isReady = true }))
}
})

View file

@ -11,10 +11,31 @@
let sectionEl = $state(null)
// --- Derived ---
const isActive = $derived(slides.active?.id === 'portfolio')
const projects = $derived(data?.projects ?? [])
const isActive = $derived(slides.active?.id === 'portfolio')
const projects = $derived(data?.projects ?? [])
const currentProject = $derived(projects[currentIndex] ?? null)
// Capture du hash synchrone avant que tout effect puisse le modifier
const initialHash = window.location.hash.slice(1)
// --- Ancres ---
function setAnchor(index) {
const slug = projects[index]?.slug
if (!slug) return
history.replaceState(null, '', '#' + slug)
}
function clearAnchor() {
history.replaceState(null, '', window.location.pathname + window.location.search)
}
// Initialisation depuis l'ancre URL — une seule fois quand projects est prêt
$effect(() => {
if (projects.length === 0 || !initialHash) return
const idx = projects.findIndex(p => p.slug === initialHash)
if (idx > 0) currentIndex = idx
})
// --- Scroll nav composable ---
const nav = createScrollNav({
isActive: () => isActive,
@ -24,6 +45,7 @@
: Math.max(currentIndex - 1, 0)
if (next === currentIndex) return false
currentIndex = next
setAnchor(next)
},
})
@ -40,10 +62,17 @@
})
// --- Effect: reset when slide deactivated ---
// wasActive évite que clearAnchor() s'exécute au montage initial
// (isActive est false avant l'initialisation des slides)
let wasActive = false
$effect(() => {
if (!isActive) {
if (isActive) {
wasActive = true
} else if (wasActive) {
nav.reset()
currentIndex = 0
clearAnchor()
wasActive = false
}
})
</script>
@ -97,7 +126,7 @@
<button
class="portfolio-nav-item"
class:active={i === currentIndex}
onclick={() => { currentIndex = i }}
onclick={() => { currentIndex = i; setAnchor(i) }}
>
<span class="portfolio-nav-number">{String(i + 1).padStart(2, '0')}</span>
<img src={project.thumbnail} alt={project.title} />
@ -207,7 +236,7 @@
}
.portfolio-nav-item.active {
transform: scale(1.25);
transform: scale(1.25) translateX(-50%);
opacity: 1;
}