Feat: page Portfolio avec galerie animée, navigation par scroll/touch/clavier
All checks were successful
Deploy / Deploy to Production (push) Successful in 18s
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:
parent
feb300f76e
commit
0b563b4697
9 changed files with 505 additions and 125 deletions
88
src/composables/useScrollNav.svelte.js
Normal file
88
src/composables/useScrollNav.svelte.js
Normal 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,
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue