expertise : simplify video + text scroll logic. related to #52
All checks were successful
Deploy / Deploy to Production (push) Successful in 25s
All checks were successful
Deploy / Deploy to Production (push) Successful in 25s
Replace complex segment-based video control with a simpler model: - Scroll down → advance one text item (650ms lock) + play video forward - Scroll up → go back one text item + play video in reverse - Video plays continuously in chosen direction, decoupled from text items - Remove useScrollNav, segmentEnds, offsetY, computeOffset, fwdTarget/revTarget Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
0afbcf4088
commit
6ec32dd82a
1 changed files with 159 additions and 236 deletions
|
|
@ -2,255 +2,190 @@
|
||||||
import { onMount } from 'svelte'
|
import { onMount } from 'svelte'
|
||||||
import { slides } from '@state/slides.svelte'
|
import { slides } from '@state/slides.svelte'
|
||||||
import { navigation } from '@state/navigation.svelte'
|
import { navigation } from '@state/navigation.svelte'
|
||||||
import { createScrollNav } from '@composables/useScrollNav.svelte.js'
|
|
||||||
|
|
||||||
let { data } = $props()
|
let { data } = $props()
|
||||||
|
|
||||||
// --- Constants ---
|
// --- DOM refs ---
|
||||||
const PLAY_DELAY_MS = 300 // ms delay before starting playback on slide activation
|
let videoFwd = $state(null)
|
||||||
|
let videoRev = $state(null)
|
||||||
|
let sectionEl = $state(null)
|
||||||
|
|
||||||
// --- State ---
|
// --- State ---
|
||||||
let currentItem = $state(0)
|
let isReverse = $state(false)
|
||||||
let isReverse = $state(false)
|
let duration = $state(0)
|
||||||
let offsetY = $state(0)
|
let currentItem = $state(0)
|
||||||
let videoDuration = $state(0)
|
|
||||||
|
|
||||||
// --- DOM refs ---
|
// --- Non-reactive ---
|
||||||
let videoFwd = $state(null)
|
let playhead = 0 // forward-equivalent position, updated from timeupdate
|
||||||
let videoRev = $state(null)
|
let switching = false
|
||||||
let textContainer = $state(null)
|
let canScroll = true
|
||||||
let itemEls = $state([])
|
let lockTimer = null
|
||||||
let sectionEl = $state(null)
|
|
||||||
|
|
||||||
// --- Plain variables (not reactive, used in event listeners) ---
|
|
||||||
let fwdTarget = null
|
|
||||||
let revTarget = null
|
|
||||||
let currentFwdTime = 0
|
|
||||||
|
|
||||||
// --- Derived ---
|
// --- Derived ---
|
||||||
const isActive = $derived(slides.active?.id === 'expertise')
|
const isActive = $derived(slides.active?.id === 'expertise')
|
||||||
const items = $derived(data?.items ?? [])
|
const items = $derived(data?.items ?? [])
|
||||||
const itemCount = $derived(items.length)
|
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (isActive) navigation.setScrolled(currentItem > 0)
|
if (isActive) navigation.setScrolled(currentItem > 0)
|
||||||
})
|
})
|
||||||
|
|
||||||
// segmentEnds[i] = position forward correspondant à l'item i.
|
// --- Video init / reset ---
|
||||||
// Item 0 = début de vidéo (0), item final = fin de vidéo (duration).
|
|
||||||
const segmentEnds = $derived(
|
|
||||||
itemCount > 1 && videoDuration > 0
|
|
||||||
? Array.from({ length: itemCount }, (_, i) => videoDuration * i / (itemCount - 1))
|
|
||||||
: itemCount === 1 ? [0]
|
|
||||||
: []
|
|
||||||
)
|
|
||||||
|
|
||||||
// --- Scroll nav composable ---
|
$effect(() => {
|
||||||
const nav = createScrollNav({
|
if (isActive) {
|
||||||
isActive: () => isActive,
|
initVideo()
|
||||||
onNavigate: (dir) => {
|
} else {
|
||||||
if (segmentEnds.length === 0) return false
|
resetVideo()
|
||||||
const prevItem = currentItem
|
}
|
||||||
const newItem = dir === 'down'
|
|
||||||
? Math.min(prevItem + 1, itemCount - 1)
|
|
||||||
: Math.max(prevItem - 1, 0)
|
|
||||||
if (newItem === prevItem) return false
|
|
||||||
navigate(dir, newItem)
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// --- Center active item vertically in viewport ---
|
function initVideo() {
|
||||||
function computeOffset() {
|
if (!videoFwd) return
|
||||||
if (!textContainer || !itemEls[currentItem]) return
|
const start = () => {
|
||||||
const wh = window.innerHeight
|
duration = videoFwd.duration
|
||||||
const wrapperRect = textContainer.parentElement.getBoundingClientRect()
|
videoFwd.currentTime = 0
|
||||||
const el = itemEls[currentItem]
|
playhead = 0
|
||||||
offsetY = wh / 2 - wrapperRect.top - el.offsetTop - el.offsetHeight / 2
|
isReverse = false
|
||||||
}
|
if (videoRev) videoRev.currentTime = duration
|
||||||
|
// Force first-frame decode (required on mobile Safari)
|
||||||
// --- Video control helpers ---
|
videoFwd.play().then(() => videoFwd.pause()).catch(() => {})
|
||||||
|
|
||||||
/** Stoppe la vidéo active et sauvegarde la position forward-équivalente. */
|
|
||||||
function stopActiveVideo() {
|
|
||||||
if (videoFwd && !videoFwd.paused) {
|
|
||||||
currentFwdTime = videoFwd.currentTime
|
|
||||||
videoFwd.pause()
|
|
||||||
fwdTarget = null
|
|
||||||
}
|
|
||||||
if (videoRev && !videoRev.paused) {
|
|
||||||
currentFwdTime = videoDuration - videoRev.currentTime
|
|
||||||
videoRev.pause()
|
|
||||||
revTarget = null
|
|
||||||
}
|
}
|
||||||
|
if (videoFwd.readyState >= 1) start()
|
||||||
|
else videoFwd.addEventListener('loadedmetadata', start, { once: true })
|
||||||
}
|
}
|
||||||
|
|
||||||
function playForward(targetTime) {
|
function resetVideo() {
|
||||||
fwdTarget = targetTime
|
clearTimeout(lockTimer)
|
||||||
|
switching = false
|
||||||
if (!isReverse) {
|
canScroll = true
|
||||||
// Déjà sur forward : seek + play directement
|
playhead = 0
|
||||||
if (videoFwd) videoFwd.currentTime = currentFwdTime
|
|
||||||
videoFwd?.play().catch(() => {})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Changement de direction : seek forward, attendre seeked, puis switcher
|
|
||||||
if (videoFwd) videoFwd.currentTime = currentFwdTime
|
|
||||||
videoFwd?.addEventListener('seeked', () => {
|
|
||||||
isReverse = false // maintenant forward est au bon frame → on l'affiche
|
|
||||||
videoFwd?.play().catch(() => {})
|
|
||||||
}, { once: true })
|
|
||||||
}
|
|
||||||
|
|
||||||
function playReverse(targetFwdPos) {
|
|
||||||
revTarget = videoDuration - Math.max(targetFwdPos, 0)
|
|
||||||
const revStart = videoDuration - currentFwdTime
|
|
||||||
|
|
||||||
if (isReverse) {
|
|
||||||
// Déjà sur reverse : seek + play directement
|
|
||||||
if (videoRev) videoRev.currentTime = revStart
|
|
||||||
videoRev?.play().catch(() => {})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Changement de direction : seek reverse, attendre seeked, puis switcher
|
|
||||||
if (videoRev) videoRev.currentTime = revStart
|
|
||||||
videoRev?.addEventListener('seeked', () => {
|
|
||||||
isReverse = true // maintenant reverse est au bon frame → on l'affiche
|
|
||||||
videoRev?.play().catch(() => {})
|
|
||||||
}, { once: true })
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Navigate: move one item up or down (called by composable) ---
|
|
||||||
function navigate(direction, newItem) {
|
|
||||||
currentItem = newItem
|
|
||||||
const targetPos = segmentEnds[currentItem]
|
|
||||||
|
|
||||||
if (direction === 'down') {
|
|
||||||
if (videoFwd && !videoFwd.paused) {
|
|
||||||
// Forward déjà en cours : juste mettre à jour la cible
|
|
||||||
fwdTarget = targetPos
|
|
||||||
} else {
|
|
||||||
stopActiveVideo()
|
|
||||||
playForward(targetPos)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (videoRev && !videoRev.paused) {
|
|
||||||
// Reverse déjà en cours : juste mettre à jour la cible
|
|
||||||
revTarget = videoDuration - Math.max(targetPos, 0)
|
|
||||||
} else {
|
|
||||||
stopActiveVideo()
|
|
||||||
playReverse(targetPos)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
requestAnimationFrame(() => { if (isActive) computeOffset() })
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Playback lifecycle ---
|
|
||||||
function initPlayback() {
|
|
||||||
setTimeout(() => {
|
|
||||||
const init = () => {
|
|
||||||
const dur = videoFwd?.duration
|
|
||||||
if (!dur || itemCount === 0) return
|
|
||||||
videoDuration = dur
|
|
||||||
videoFwd.currentTime = 0
|
|
||||||
currentFwdTime = 0
|
|
||||||
isReverse = false
|
|
||||||
if (videoRev) videoRev.currentTime = dur
|
|
||||||
requestAnimationFrame(() => computeOffset())
|
|
||||||
// Force le décodage de la première frame (nécessaire sur mobile)
|
|
||||||
videoFwd.play().then(() => {
|
|
||||||
videoFwd.pause()
|
|
||||||
videoFwd.currentTime = 0
|
|
||||||
}).catch(() => {})
|
|
||||||
}
|
|
||||||
if (videoFwd?.duration) {
|
|
||||||
init()
|
|
||||||
} else {
|
|
||||||
videoFwd?.addEventListener('loadedmetadata', () => {
|
|
||||||
videoDuration = videoFwd.duration
|
|
||||||
init()
|
|
||||||
}, { once: true })
|
|
||||||
}
|
|
||||||
}, PLAY_DELAY_MS)
|
|
||||||
}
|
|
||||||
|
|
||||||
function resetPlayback() {
|
|
||||||
fwdTarget = null
|
|
||||||
revTarget = null
|
|
||||||
currentFwdTime = 0
|
|
||||||
if (videoFwd) { videoFwd.pause(); videoFwd.currentTime = 0 }
|
|
||||||
if (videoRev) { videoRev.pause(); videoRev.currentTime = 0 }
|
|
||||||
currentItem = 0
|
|
||||||
offsetY = 0
|
|
||||||
isReverse = false
|
isReverse = false
|
||||||
nav.reset()
|
currentItem = 0
|
||||||
|
if (videoFwd) { videoFwd.pause(); videoFwd.currentTime = 0 }
|
||||||
|
if (videoRev) { videoRev.pause() }
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Playback ---
|
||||||
|
|
||||||
|
function pauseAll() {
|
||||||
|
videoFwd?.pause()
|
||||||
|
videoRev?.pause()
|
||||||
|
}
|
||||||
|
|
||||||
|
function playForward() {
|
||||||
|
if (switching) return
|
||||||
|
if (!isReverse) {
|
||||||
|
videoFwd?.play().catch(() => {})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Switch reverse → forward
|
||||||
|
switching = true
|
||||||
|
pauseAll()
|
||||||
|
const pos = duration - (videoRev?.currentTime ?? 0)
|
||||||
|
playhead = pos
|
||||||
|
if (!videoFwd) { switching = false; return }
|
||||||
|
videoFwd.currentTime = pos
|
||||||
|
videoFwd.addEventListener('seeked', () => {
|
||||||
|
isReverse = false
|
||||||
|
switching = false
|
||||||
|
videoFwd.play().catch(() => {})
|
||||||
|
}, { once: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
function playReverse() {
|
||||||
|
if (switching) return
|
||||||
|
if (isReverse) {
|
||||||
|
videoRev?.play().catch(() => {})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Switch forward → reverse
|
||||||
|
switching = true
|
||||||
|
pauseAll()
|
||||||
|
const pos = videoFwd?.currentTime ?? playhead
|
||||||
|
playhead = pos
|
||||||
|
if (!videoRev) { switching = false; return }
|
||||||
|
videoRev.currentTime = duration - pos
|
||||||
|
videoRev.addEventListener('seeked', () => {
|
||||||
|
isReverse = true
|
||||||
|
switching = false
|
||||||
|
videoRev.play().catch(() => {})
|
||||||
|
}, { once: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
function navigate(dir) {
|
||||||
|
if (!isActive || !canScroll) return
|
||||||
|
|
||||||
|
const next = dir === 'down'
|
||||||
|
? Math.min(currentItem + 1, items.length - 1)
|
||||||
|
: Math.max(currentItem - 1, 0)
|
||||||
|
|
||||||
|
if (next === currentItem) return // à la limite, ne rien faire
|
||||||
|
|
||||||
|
currentItem = next
|
||||||
|
canScroll = false
|
||||||
|
clearTimeout(lockTimer)
|
||||||
|
lockTimer = setTimeout(() => { canScroll = true }, 650)
|
||||||
|
|
||||||
|
dir === 'down' ? playForward() : playReverse()
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- onMount ---
|
// --- onMount ---
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
sectionEl?.addEventListener('wheel', nav.onWheel, { passive: false })
|
// Maintenir playhead (non-réactif) pour les direction switches
|
||||||
|
const onFwdTime = () => { if (videoFwd && !videoFwd.paused) playhead = videoFwd.currentTime }
|
||||||
|
const onRevTime = () => { if (videoRev && !videoRev.paused) playhead = duration - videoRev.currentTime }
|
||||||
|
const onFwdEnded = () => { playhead = duration }
|
||||||
|
const onRevEnded = () => { playhead = 0 }
|
||||||
|
|
||||||
const onFwdUpdate = () => {
|
videoFwd?.addEventListener('timeupdate', onFwdTime)
|
||||||
if (!videoFwd || videoFwd.paused || fwdTarget === null) return
|
videoRev?.addEventListener('timeupdate', onRevTime)
|
||||||
if (videoFwd.currentTime >= fwdTarget - 0.05) {
|
|
||||||
currentFwdTime = fwdTarget
|
|
||||||
videoFwd.pause()
|
|
||||||
fwdTarget = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
videoFwd?.addEventListener('timeupdate', onFwdUpdate)
|
|
||||||
|
|
||||||
// Quand la vidéo forward atteint la fin naturellement, timeupdate
|
|
||||||
// peut ne pas fire avec currentTime >= duration. ended le garantit.
|
|
||||||
const onFwdEnded = () => {
|
|
||||||
currentFwdTime = videoDuration
|
|
||||||
fwdTarget = null
|
|
||||||
}
|
|
||||||
videoFwd?.addEventListener('ended', onFwdEnded)
|
videoFwd?.addEventListener('ended', onFwdEnded)
|
||||||
|
|
||||||
const onRevUpdate = () => {
|
|
||||||
if (!videoRev || videoRev.paused || revTarget === null) return
|
|
||||||
if (videoRev.currentTime >= revTarget - 0.05) {
|
|
||||||
const fwdPos = videoDuration - revTarget
|
|
||||||
currentFwdTime = Math.max(fwdPos, 0)
|
|
||||||
videoRev.pause()
|
|
||||||
revTarget = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
videoRev?.addEventListener('timeupdate', onRevUpdate)
|
|
||||||
|
|
||||||
const onRevEnded = () => {
|
|
||||||
currentFwdTime = 0
|
|
||||||
revTarget = null
|
|
||||||
}
|
|
||||||
videoRev?.addEventListener('ended', onRevEnded)
|
videoRev?.addEventListener('ended', onRevEnded)
|
||||||
|
|
||||||
const onResize = () => { if (isActive) computeOffset() }
|
// Wheel
|
||||||
window.addEventListener('resize', onResize)
|
let scrollDelta = 0
|
||||||
window.addEventListener('orientationchange', onResize)
|
let lastScrollAt = 0
|
||||||
window.addEventListener('keydown', nav.onKeyDown)
|
const onWheel = (e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
const now = Date.now()
|
||||||
|
if (now - lastScrollAt > 100) scrollDelta = 0
|
||||||
|
lastScrollAt = now
|
||||||
|
scrollDelta += e.deltaY
|
||||||
|
if (Math.abs(scrollDelta) >= 25) {
|
||||||
|
navigate(scrollDelta > 0 ? 'down' : 'up')
|
||||||
|
scrollDelta = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sectionEl?.addEventListener('wheel', onWheel, { passive: false })
|
||||||
|
|
||||||
|
// Touch
|
||||||
|
let touchStartY = 0
|
||||||
|
const onTouchStart = (e) => { touchStartY = e.touches[0].clientY }
|
||||||
|
const onTouchEnd = (e) => {
|
||||||
|
const delta = touchStartY - e.changedTouches[0].clientY
|
||||||
|
if (Math.abs(delta) > 50) navigate(delta > 0 ? 'down' : 'up')
|
||||||
|
}
|
||||||
|
sectionEl?.addEventListener('touchstart', onTouchStart, { passive: true })
|
||||||
|
sectionEl?.addEventListener('touchend', onTouchEnd, { passive: true })
|
||||||
|
|
||||||
|
// Keyboard
|
||||||
|
const onKeyDown = (e) => {
|
||||||
|
if (!isActive) return
|
||||||
|
if (e.key === 'ArrowDown') { e.preventDefault(); navigate('down') }
|
||||||
|
if (e.key === 'ArrowUp') { e.preventDefault(); navigate('up') }
|
||||||
|
}
|
||||||
|
window.addEventListener('keydown', onKeyDown)
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
sectionEl?.removeEventListener('wheel', nav.onWheel)
|
sectionEl?.removeEventListener('wheel', onWheel)
|
||||||
videoFwd?.removeEventListener('timeupdate', onFwdUpdate)
|
sectionEl?.removeEventListener('touchstart', onTouchStart)
|
||||||
|
sectionEl?.removeEventListener('touchend', onTouchEnd)
|
||||||
|
window.removeEventListener('keydown', onKeyDown)
|
||||||
|
videoFwd?.removeEventListener('timeupdate', onFwdTime)
|
||||||
|
videoRev?.removeEventListener('timeupdate', onRevTime)
|
||||||
videoFwd?.removeEventListener('ended', onFwdEnded)
|
videoFwd?.removeEventListener('ended', onFwdEnded)
|
||||||
videoRev?.removeEventListener('timeupdate', onRevUpdate)
|
|
||||||
videoRev?.removeEventListener('ended', onRevEnded)
|
videoRev?.removeEventListener('ended', onRevEnded)
|
||||||
window.removeEventListener('resize', onResize)
|
clearTimeout(lockTimer)
|
||||||
window.removeEventListener('orientationchange', onResize)
|
|
||||||
window.removeEventListener('keydown', nav.onKeyDown)
|
|
||||||
nav.destroy()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// --- Effect: react to slide activation / deactivation ---
|
|
||||||
$effect(() => {
|
|
||||||
if (isActive) {
|
|
||||||
initPlayback()
|
|
||||||
} else {
|
|
||||||
resetPlayback()
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -259,8 +194,6 @@
|
||||||
class="expertise golden-grid"
|
class="expertise golden-grid"
|
||||||
aria-label="Expertise"
|
aria-label="Expertise"
|
||||||
bind:this={sectionEl}
|
bind:this={sectionEl}
|
||||||
ontouchstart={nav.onTouchStart}
|
|
||||||
ontouchend={nav.onTouchEnd}
|
|
||||||
>
|
>
|
||||||
<!-- Video background (decorative) -->
|
<!-- Video background (decorative) -->
|
||||||
<div class="expertise-bg" aria-hidden="true">
|
<div class="expertise-bg" aria-hidden="true">
|
||||||
|
|
@ -289,19 +222,11 @@
|
||||||
{@html data?.pageTitle ?? ''}
|
{@html data?.pageTitle ?? ''}
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<!-- Sliding text container -->
|
<!-- Text items -->
|
||||||
<div class="expertise-text-wrapper" aria-live="polite" aria-atomic="true">
|
<div class="expertise-text-wrapper" aria-live="polite" aria-atomic="true">
|
||||||
<div
|
<div class="expertise-text">
|
||||||
class="expertise-text"
|
|
||||||
bind:this={textContainer}
|
|
||||||
style="transform: translateY({offsetY}px)"
|
|
||||||
>
|
|
||||||
{#each items as item, i}
|
{#each items as item, i}
|
||||||
<div
|
<div class="expertise-item" class:active={i === currentItem}>
|
||||||
class="expertise-item"
|
|
||||||
class:active={i === currentItem}
|
|
||||||
bind:this={itemEls[i]}
|
|
||||||
>
|
|
||||||
{@html item.text}
|
{@html item.text}
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
@ -343,7 +268,7 @@
|
||||||
grid-area: 8/6 / span 5 / span 5;
|
grid-area: 8/6 / span 5 / span 5;
|
||||||
z-index: var(--z-content);
|
z-index: var(--z-content);
|
||||||
align-self: center;
|
align-self: center;
|
||||||
|
|
||||||
font-size: var(--font-size-title-hero);
|
font-size: var(--font-size-title-hero);
|
||||||
text-align: left;
|
text-align: left;
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
|
|
@ -358,14 +283,14 @@
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Sliding container */
|
/* Text container */
|
||||||
.expertise-text {
|
.expertise-text {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.expertise-text, .expertise-item {
|
.expertise-item {
|
||||||
transition: all 0.6s var(--ease-standard);
|
transition: opacity 0.6s var(--ease-standard), transform 0.6s var(--ease-standard);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Individual text items */
|
/* Individual text items */
|
||||||
|
|
@ -384,7 +309,6 @@
|
||||||
.expertise-item.active {
|
.expertise-item.active {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transform: scale(1) translateX(0);
|
transform: scale(1) translateX(0);
|
||||||
margin-left: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Mobile (≤ 700px) */
|
/* Mobile (≤ 700px) */
|
||||||
|
|
@ -422,7 +346,6 @@
|
||||||
|
|
||||||
/* Reduced motion */
|
/* Reduced motion */
|
||||||
@media (prefers-reduced-motion: reduce) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
.expertise-text,
|
|
||||||
.expertise-item {
|
.expertise-item {
|
||||||
transition: none;
|
transition: none;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue