Feat: page Portfolio avec galerie animée, navigation par scroll/touch/clavier
All checks were successful
Deploy / Deploy to Production (push) Successful in 18s

- Composable useScrollNav partagé entre Expertise et Portfolio (wheel/touch/clavier)
- GalleryAnimation : 3 colonnes CSS défilantes infinies avec décalage et delay
- Portfolio : golden grid, mockup centré, infos projet, sidebar vignettes navigables
- API portfolio.json.php alignée sur blueprint project.yml (catchphrase, images_gallery, mockup, keywords, external_links)
- Variable --ease-standard partagée dans variables.css
- Alias @composables ajouté dans vite.config.js
- Refactor Expertise pour utiliser le composable (comportement identique)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
isUnknown 2026-03-05 17:13:50 +01:00
parent feb300f76e
commit 0b563b4697
9 changed files with 505 additions and 125 deletions

View file

@ -0,0 +1,88 @@
/**
* createScrollNav composable partagé scroll/touch/clavier pour Expertise et Portfolio.
*
* @param {object} options
* @param {() => boolean} options.isActive getter réactif : la slide est-elle active ?
* @param {(dir: 'up'|'down') => false|void} options.onNavigate appelé pour naviguer ;
* retourner `false` si on est à la limite
* @param {object} [options.config]
* @param {number} [options.config.scrollLockMs=650]
* @param {number} [options.config.wheelDebounceMs=100]
* @param {number} [options.config.wheelThreshold=25]
* @param {number} [options.config.touchThreshold=50]
*/
export function createScrollNav({ isActive, onNavigate, config = {} }) {
const {
scrollLockMs = 650,
wheelDebounceMs = 100,
wheelThreshold = 25,
touchThreshold = 50,
} = config
let canScroll = $state(true)
let lockTimer = null
let scrollDelta = 0
let lastScrollAt = 0
let touchStartY = 0
function lock() {
canScroll = false
clearTimeout(lockTimer)
lockTimer = setTimeout(() => { canScroll = true }, scrollLockMs)
}
function tryNavigate(dir) {
const result = onNavigate(dir)
if (result !== false) lock()
}
function onWheel(e) {
e.preventDefault()
if (!isActive() || !canScroll) return
const now = Date.now()
if (now - lastScrollAt > wheelDebounceMs) scrollDelta = 0
lastScrollAt = now
scrollDelta += e.deltaY
if (Math.abs(scrollDelta) >= wheelThreshold) {
tryNavigate(scrollDelta > 0 ? 'down' : 'up')
scrollDelta = 0
}
}
function onTouchStart(e) {
touchStartY = e.touches[0].clientY
}
function onTouchEnd(e) {
if (!isActive() || !canScroll) return
const diff = touchStartY - e.changedTouches[0].clientY
if (Math.abs(diff) >= touchThreshold) tryNavigate(diff > 0 ? 'down' : 'up')
}
function onKeyDown(e) {
if (!isActive() || !canScroll) return
if (e.key === 'ArrowDown') { e.preventDefault(); tryNavigate('down') }
if (e.key === 'ArrowUp') { e.preventDefault(); tryNavigate('up') }
}
function reset() {
canScroll = true
clearTimeout(lockTimer)
lockTimer = null
scrollDelta = 0
}
function destroy() {
clearTimeout(lockTimer)
}
return {
get canScroll() { return canScroll },
onWheel,
onTouchStart,
onTouchEnd,
onKeyDown,
reset,
destroy,
}
}