Migration vers architecture Svelte + Kirby inspirée de design-to-pack
- Mise en place de Svelte 4 avec Vite pour le frontend (SPA)
- Simplification des templates PHP (header/footer minimalistes)
- Création de templates JSON pour API (home, about, expertise, portfolio, jouer, game, blog, article, project)
- Ajout d'un controller de site pour définir genericData globalement
- Structure des stores Svelte (page, navigation, locale, site)
- Router avec navaid pour navigation SPA et interception des liens
- Composants layout (Header, Footer, Cursor) et vues de base
- Build Vite vers assets/dist/ (index.js/css)
- Header PHP détecte assets/dist pour basculer dev/prod
Architecture fonctionnelle de base établie, à améliorer et compléter.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-06 16:30:15 +01:00
|
|
|
|
<script>
|
2026-02-19 18:54:25 +01:00
|
|
|
|
import { onMount } from 'svelte'
|
|
|
|
|
|
import { slides } from '@state/slides.svelte'
|
2026-03-12 15:05:25 +01:00
|
|
|
|
import { navigation } from '@state/navigation.svelte'
|
2026-03-05 17:13:50 +01:00
|
|
|
|
import { createScrollNav } from '@composables/useScrollNav.svelte.js'
|
2026-02-19 18:54:25 +01:00
|
|
|
|
|
2026-02-07 08:26:28 +01:00
|
|
|
|
let { data } = $props()
|
2026-02-19 18:54:25 +01:00
|
|
|
|
|
2026-02-19 20:38:55 +01:00
|
|
|
|
// --- Constants ---
|
2026-03-05 17:13:50 +01:00
|
|
|
|
const PLAY_DELAY_MS = 300 // ms delay before starting playback on slide activation
|
|
|
|
|
|
const REV_TARGET_MIN = 0.1 // min revTarget to avoid pausing exactly at videoDuration
|
2026-02-19 20:38:55 +01:00
|
|
|
|
|
2026-02-19 18:54:25 +01:00
|
|
|
|
// --- State ---
|
|
|
|
|
|
let currentItem = $state(0)
|
|
|
|
|
|
let isReverse = $state(false)
|
|
|
|
|
|
let offsetY = $state(0)
|
|
|
|
|
|
let videoDuration = $state(0)
|
|
|
|
|
|
|
|
|
|
|
|
// --- DOM refs ---
|
|
|
|
|
|
let videoFwd = $state(null)
|
|
|
|
|
|
let videoRev = $state(null)
|
|
|
|
|
|
let textContainer = $state(null)
|
|
|
|
|
|
let itemEls = $state([])
|
|
|
|
|
|
let sectionEl = $state(null)
|
|
|
|
|
|
|
|
|
|
|
|
// --- Plain variables (not reactive, used in event listeners) ---
|
2026-03-05 17:13:50 +01:00
|
|
|
|
let fwdTarget = null
|
|
|
|
|
|
let revTarget = null
|
2026-02-19 20:38:55 +01:00
|
|
|
|
let currentFwdTime = 0
|
2026-02-19 18:54:25 +01:00
|
|
|
|
|
2026-02-19 20:38:55 +01:00
|
|
|
|
// --- Derived ---
|
|
|
|
|
|
const isActive = $derived(slides.active?.id === 'expertise')
|
|
|
|
|
|
const items = $derived(data?.items ?? [])
|
|
|
|
|
|
const itemCount = $derived(items.length)
|
2026-02-19 18:54:25 +01:00
|
|
|
|
|
2026-03-12 15:05:25 +01:00
|
|
|
|
$effect(() => {
|
|
|
|
|
|
if (isActive) navigation.setScrolled(currentItem > 0)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-02-19 18:54:25 +01:00
|
|
|
|
const segmentEnds = $derived(
|
2026-02-19 20:38:55 +01:00
|
|
|
|
itemCount > 0 && videoDuration > 0
|
|
|
|
|
|
? Array.from({ length: itemCount }, (_, i) => videoDuration * (i + 1) / itemCount)
|
2026-02-19 18:54:25 +01:00
|
|
|
|
: []
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-03-05 17:13:50 +01:00
|
|
|
|
// --- Scroll nav composable ---
|
|
|
|
|
|
const nav = createScrollNav({
|
|
|
|
|
|
isActive: () => isActive,
|
|
|
|
|
|
onNavigate: (dir) => {
|
|
|
|
|
|
if (segmentEnds.length === 0) return false
|
|
|
|
|
|
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)
|
|
|
|
|
|
},
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-02-19 18:54:25 +01:00
|
|
|
|
// --- Center active item vertically in viewport ---
|
|
|
|
|
|
function computeOffset() {
|
|
|
|
|
|
if (!textContainer || !itemEls[currentItem]) return
|
|
|
|
|
|
const wh = window.innerHeight
|
|
|
|
|
|
const wrapperRect = textContainer.parentElement.getBoundingClientRect()
|
|
|
|
|
|
const el = itemEls[currentItem]
|
|
|
|
|
|
offsetY = wh / 2 - wrapperRect.top - el.offsetTop - el.offsetHeight / 2
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-19 20:38:55 +01:00
|
|
|
|
// --- Video control helpers ---
|
|
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function playForward(targetTime) {
|
|
|
|
|
|
fwdTarget = targetTime
|
|
|
|
|
|
if (videoFwd) videoFwd.currentTime = currentFwdTime
|
|
|
|
|
|
if (isReverse) {
|
|
|
|
|
|
videoFwd?.addEventListener('seeked', () => {
|
|
|
|
|
|
isReverse = false
|
|
|
|
|
|
videoFwd?.play().catch(() => {})
|
|
|
|
|
|
}, { once: true })
|
|
|
|
|
|
} else {
|
|
|
|
|
|
videoFwd?.play().catch(() => {})
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function playReverse(targetTime) {
|
|
|
|
|
|
const revStart = Math.max(0, videoDuration - currentFwdTime)
|
|
|
|
|
|
revTarget = targetTime
|
|
|
|
|
|
if (videoRev) videoRev.currentTime = revStart
|
|
|
|
|
|
if (!isReverse) {
|
|
|
|
|
|
videoRev?.addEventListener('seeked', () => {
|
|
|
|
|
|
isReverse = true
|
|
|
|
|
|
videoRev?.play().catch(() => {})
|
|
|
|
|
|
}, { once: true })
|
|
|
|
|
|
} else {
|
|
|
|
|
|
videoRev?.play().catch(() => {})
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-05 17:13:50 +01:00
|
|
|
|
// --- Navigate: move one item up or down (called by composable) ---
|
|
|
|
|
|
function navigate(direction, newItem) {
|
|
|
|
|
|
const prevItem = currentItem
|
|
|
|
|
|
currentItem = newItem
|
2026-02-19 18:54:25 +01:00
|
|
|
|
|
|
|
|
|
|
if (direction === 'down' && videoFwd && !videoFwd.paused) {
|
|
|
|
|
|
fwdTarget = segmentEnds[currentItem]
|
|
|
|
|
|
} else if (direction === 'up' && videoRev && !videoRev.paused) {
|
|
|
|
|
|
const liveFwdPos = videoDuration - videoRev.currentTime
|
|
|
|
|
|
const segBoundary = segmentEnds[currentItem]
|
|
|
|
|
|
const targetFwd = liveFwdPos > segBoundary ? segBoundary : 0
|
2026-02-19 20:38:55 +01:00
|
|
|
|
revTarget = videoDuration - Math.max(targetFwd, REV_TARGET_MIN)
|
2026-02-19 18:54:25 +01:00
|
|
|
|
} else {
|
2026-02-19 20:38:55 +01:00
|
|
|
|
stopActiveVideo()
|
2026-02-19 18:54:25 +01:00
|
|
|
|
if (direction === 'down') {
|
2026-02-19 20:38:55 +01:00
|
|
|
|
playForward(segmentEnds[currentItem])
|
2026-02-19 18:54:25 +01:00
|
|
|
|
} else {
|
|
|
|
|
|
const segBoundary = segmentEnds[currentItem]
|
2026-02-19 20:38:55 +01:00
|
|
|
|
const targetFwd = currentFwdTime > segBoundary ? segBoundary : 0
|
|
|
|
|
|
playReverse(videoDuration - Math.max(targetFwd, REV_TARGET_MIN))
|
2026-02-19 18:54:25 +01:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
requestAnimationFrame(() => { if (isActive) computeOffset() })
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-19 20:38:55 +01:00
|
|
|
|
// --- Playback lifecycle ---
|
|
|
|
|
|
function initPlayback() {
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
const tryPlay = () => {
|
|
|
|
|
|
const dur = videoFwd?.duration
|
|
|
|
|
|
if (!dur || itemCount === 0) return
|
|
|
|
|
|
videoDuration = dur
|
|
|
|
|
|
videoFwd.currentTime = 0
|
|
|
|
|
|
currentFwdTime = 0
|
|
|
|
|
|
isReverse = false
|
2026-03-05 17:13:50 +01:00
|
|
|
|
fwdTarget = dur / itemCount
|
2026-02-19 20:38:55 +01:00
|
|
|
|
videoFwd.play().catch(() => {})
|
|
|
|
|
|
if (videoRev) videoRev.currentTime = Math.max(0, dur - dur / itemCount)
|
|
|
|
|
|
requestAnimationFrame(() => computeOffset())
|
|
|
|
|
|
}
|
|
|
|
|
|
if (videoFwd?.duration) {
|
|
|
|
|
|
tryPlay()
|
|
|
|
|
|
} else {
|
|
|
|
|
|
videoFwd?.addEventListener('loadedmetadata', () => {
|
|
|
|
|
|
videoDuration = videoFwd.duration
|
|
|
|
|
|
tryPlay()
|
|
|
|
|
|
}, { 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
|
2026-03-05 17:13:50 +01:00
|
|
|
|
nav.reset()
|
2026-02-19 18:54:25 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-05 17:13:50 +01:00
|
|
|
|
// --- onMount ---
|
2026-02-19 18:54:25 +01:00
|
|
|
|
onMount(() => {
|
2026-03-05 17:13:50 +01:00
|
|
|
|
sectionEl?.addEventListener('wheel', nav.onWheel, { passive: false })
|
2026-02-19 18:54:25 +01:00
|
|
|
|
|
|
|
|
|
|
const onFwdUpdate = () => {
|
|
|
|
|
|
if (!videoFwd || videoFwd.paused || fwdTarget === null) return
|
|
|
|
|
|
if (videoFwd.currentTime >= fwdTarget) {
|
2026-02-19 20:38:55 +01:00
|
|
|
|
currentFwdTime = videoFwd.currentTime
|
2026-02-19 18:54:25 +01:00
|
|
|
|
videoFwd.pause()
|
|
|
|
|
|
fwdTarget = null
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
videoFwd?.addEventListener('timeupdate', onFwdUpdate)
|
|
|
|
|
|
|
|
|
|
|
|
const onRevUpdate = () => {
|
|
|
|
|
|
if (!videoRev || videoRev.paused || revTarget === null) return
|
|
|
|
|
|
if (videoRev.currentTime >= revTarget) {
|
2026-02-19 20:38:55 +01:00
|
|
|
|
currentFwdTime = videoDuration - videoRev.currentTime
|
2026-02-19 18:54:25 +01:00
|
|
|
|
videoRev.pause()
|
|
|
|
|
|
revTarget = null
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
videoRev?.addEventListener('timeupdate', onRevUpdate)
|
|
|
|
|
|
|
|
|
|
|
|
const onResize = () => { if (isActive) computeOffset() }
|
|
|
|
|
|
window.addEventListener('resize', onResize)
|
|
|
|
|
|
window.addEventListener('orientationchange', onResize)
|
2026-03-05 17:13:50 +01:00
|
|
|
|
window.addEventListener('keydown', nav.onKeyDown)
|
2026-02-19 18:54:25 +01:00
|
|
|
|
|
|
|
|
|
|
return () => {
|
2026-03-05 17:13:50 +01:00
|
|
|
|
sectionEl?.removeEventListener('wheel', nav.onWheel)
|
2026-02-19 18:54:25 +01:00
|
|
|
|
videoFwd?.removeEventListener('timeupdate', onFwdUpdate)
|
|
|
|
|
|
videoRev?.removeEventListener('timeupdate', onRevUpdate)
|
|
|
|
|
|
window.removeEventListener('resize', onResize)
|
|
|
|
|
|
window.removeEventListener('orientationchange', onResize)
|
2026-03-05 17:13:50 +01:00
|
|
|
|
window.removeEventListener('keydown', nav.onKeyDown)
|
|
|
|
|
|
nav.destroy()
|
2026-02-19 18:54:25 +01:00
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// --- Effect: react to slide activation / deactivation ---
|
|
|
|
|
|
$effect(() => {
|
|
|
|
|
|
if (isActive) {
|
2026-02-19 20:38:55 +01:00
|
|
|
|
initPlayback()
|
2026-02-19 18:54:25 +01:00
|
|
|
|
} else {
|
2026-02-19 20:38:55 +01:00
|
|
|
|
resetPlayback()
|
2026-02-19 18:54:25 +01:00
|
|
|
|
}
|
|
|
|
|
|
})
|
Migration vers architecture Svelte + Kirby inspirée de design-to-pack
- Mise en place de Svelte 4 avec Vite pour le frontend (SPA)
- Simplification des templates PHP (header/footer minimalistes)
- Création de templates JSON pour API (home, about, expertise, portfolio, jouer, game, blog, article, project)
- Ajout d'un controller de site pour définir genericData globalement
- Structure des stores Svelte (page, navigation, locale, site)
- Router avec navaid pour navigation SPA et interception des liens
- Composants layout (Header, Footer, Cursor) et vues de base
- Build Vite vers assets/dist/ (index.js/css)
- Header PHP détecte assets/dist pour basculer dev/prod
Architecture fonctionnelle de base établie, à améliorer et compléter.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-06 16:30:15 +01:00
|
|
|
|
</script>
|
|
|
|
|
|
|
2026-02-19 18:54:25 +01:00
|
|
|
|
<section
|
2026-03-09 18:54:51 +01:00
|
|
|
|
class="expertise golden-grid"
|
2026-02-19 18:54:25 +01:00
|
|
|
|
aria-label="Expertise"
|
|
|
|
|
|
bind:this={sectionEl}
|
2026-03-05 17:13:50 +01:00
|
|
|
|
ontouchstart={nav.onTouchStart}
|
|
|
|
|
|
ontouchend={nav.onTouchEnd}
|
2026-02-19 18:54:25 +01:00
|
|
|
|
>
|
|
|
|
|
|
<!-- Video background (decorative) -->
|
|
|
|
|
|
<div class="expertise-bg" aria-hidden="true">
|
|
|
|
|
|
<video
|
|
|
|
|
|
bind:this={videoFwd}
|
|
|
|
|
|
class:active={!isReverse}
|
|
|
|
|
|
muted
|
|
|
|
|
|
playsinline
|
|
|
|
|
|
preload="auto"
|
|
|
|
|
|
>
|
2026-03-11 15:54:49 +01:00
|
|
|
|
<source src={data.backgroundVideo ?? '/assets/video/BACKGROUND_VIDEO_MISSION.mp4'} type="video/mp4" />
|
2026-02-19 18:54:25 +01:00
|
|
|
|
</video>
|
|
|
|
|
|
<video
|
|
|
|
|
|
bind:this={videoRev}
|
|
|
|
|
|
class:active={isReverse}
|
|
|
|
|
|
muted
|
|
|
|
|
|
playsinline
|
|
|
|
|
|
preload="auto"
|
|
|
|
|
|
>
|
2026-03-11 15:54:49 +01:00
|
|
|
|
<source src={data.backgroundVideoReverse ?? '/assets/video/BACKGROUND_VIDEO_MISSION_REVERSE.mp4'} type="video/mp4" />
|
2026-02-19 18:54:25 +01:00
|
|
|
|
</video>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Decorative vertical lines -->
|
2026-03-05 15:58:29 +01:00
|
|
|
|
<div class="vertical-line-start" aria-hidden="true"></div>
|
2026-02-19 18:54:25 +01:00
|
|
|
|
<div class="vertical-line vertical-line-col8" aria-hidden="true"></div>
|
2026-03-05 15:58:29 +01:00
|
|
|
|
<div class="vertical-line-center" aria-hidden="true"></div>
|
2026-02-19 18:54:25 +01:00
|
|
|
|
<div class="vertical-line vertical-line-col14" aria-hidden="true"></div>
|
2026-03-05 15:58:29 +01:00
|
|
|
|
<div class="vertical-line-end" aria-hidden="true"></div>
|
2026-02-19 18:54:25 +01:00
|
|
|
|
|
|
|
|
|
|
<!-- Title -->
|
|
|
|
|
|
<h2 class="expertise-title font-face-terminal">
|
|
|
|
|
|
{@html data?.pageTitle ?? ''}
|
|
|
|
|
|
</h2>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Sliding text container -->
|
|
|
|
|
|
<div class="expertise-text-wrapper" aria-live="polite" aria-atomic="true">
|
|
|
|
|
|
<div
|
|
|
|
|
|
class="expertise-text"
|
|
|
|
|
|
bind:this={textContainer}
|
|
|
|
|
|
style="transform: translateY({offsetY}px)"
|
|
|
|
|
|
>
|
|
|
|
|
|
{#each items as item, i}
|
2026-02-19 19:08:38 +01:00
|
|
|
|
<div
|
2026-02-19 18:54:25 +01:00
|
|
|
|
class="expertise-item"
|
|
|
|
|
|
class:active={i === currentItem}
|
|
|
|
|
|
bind:this={itemEls[i]}
|
|
|
|
|
|
>
|
|
|
|
|
|
{@html item.text}
|
2026-02-19 19:08:38 +01:00
|
|
|
|
</div>
|
2026-02-19 18:54:25 +01:00
|
|
|
|
{/each}
|
|
|
|
|
|
</div>
|
Migration vers architecture Svelte + Kirby inspirée de design-to-pack
- Mise en place de Svelte 4 avec Vite pour le frontend (SPA)
- Simplification des templates PHP (header/footer minimalistes)
- Création de templates JSON pour API (home, about, expertise, portfolio, jouer, game, blog, article, project)
- Ajout d'un controller de site pour définir genericData globalement
- Structure des stores Svelte (page, navigation, locale, site)
- Router avec navaid pour navigation SPA et interception des liens
- Composants layout (Header, Footer, Cursor) et vues de base
- Build Vite vers assets/dist/ (index.js/css)
- Header PHP détecte assets/dist pour basculer dev/prod
Architecture fonctionnelle de base établie, à améliorer et compléter.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-06 16:30:15 +01:00
|
|
|
|
</div>
|
2026-02-19 18:54:25 +01:00
|
|
|
|
</section>
|
Migration vers architecture Svelte + Kirby inspirée de design-to-pack
- Mise en place de Svelte 4 avec Vite pour le frontend (SPA)
- Simplification des templates PHP (header/footer minimalistes)
- Création de templates JSON pour API (home, about, expertise, portfolio, jouer, game, blog, article, project)
- Ajout d'un controller de site pour définir genericData globalement
- Structure des stores Svelte (page, navigation, locale, site)
- Router avec navaid pour navigation SPA et interception des liens
- Composants layout (Header, Footer, Cursor) et vues de base
- Build Vite vers assets/dist/ (index.js/css)
- Header PHP détecte assets/dist pour basculer dev/prod
Architecture fonctionnelle de base établie, à améliorer et compléter.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-06 16:30:15 +01:00
|
|
|
|
|
|
|
|
|
|
<style>
|
|
|
|
|
|
.expertise {
|
2026-02-19 18:54:25 +01:00
|
|
|
|
background-color: #000;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-19 19:08:38 +01:00
|
|
|
|
:global(.expertise-item p) {
|
|
|
|
|
|
margin-bottom: 1rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-19 18:54:25 +01:00
|
|
|
|
/* Full-grid video background */
|
|
|
|
|
|
.expertise-bg {
|
|
|
|
|
|
grid-area: 1/1 / span 20 / span 20;
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.expertise-bg video {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
inset: 0;
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
object-fit: cover;
|
|
|
|
|
|
display: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.expertise-bg video.active {
|
|
|
|
|
|
display: block;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Custom vertical lines */
|
|
|
|
|
|
.vertical-line-col8 {
|
|
|
|
|
|
grid-area: 1/8 / span 20 / span 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.vertical-line-col14 {
|
|
|
|
|
|
grid-area: 1/14 / span 20 / span 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Title */
|
|
|
|
|
|
.expertise-title {
|
|
|
|
|
|
grid-area: 8/6 / span 5 / span 5;
|
|
|
|
|
|
z-index: var(--z-content);
|
|
|
|
|
|
text-align: left;
|
|
|
|
|
|
font-size: var(--font-size-title-hero);
|
|
|
|
|
|
color: var(--color-text);
|
|
|
|
|
|
align-self: center;
|
|
|
|
|
|
line-height: 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Wrapper clips the sliding container */
|
|
|
|
|
|
.expertise-text-wrapper {
|
|
|
|
|
|
grid-area: 8/11 / span 7 / span 6;
|
|
|
|
|
|
z-index: var(--z-content);
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: flex-start;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-05 17:13:50 +01:00
|
|
|
|
/* Sliding container */
|
2026-02-19 18:54:25 +01:00
|
|
|
|
.expertise-text {
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
width: 100%;
|
2026-03-05 16:10:27 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.expertise-text, .expertise-item {
|
2026-03-05 17:13:50 +01:00
|
|
|
|
transition: all 0.6s var(--ease-standard);
|
2026-02-19 18:54:25 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Individual text items */
|
|
|
|
|
|
.expertise-item {
|
|
|
|
|
|
font-size: var(--font-size-expertise);
|
2026-03-12 15:05:25 +01:00
|
|
|
|
font-weight: 350;
|
2026-02-19 18:54:25 +01:00
|
|
|
|
color: var(--color-text);
|
|
|
|
|
|
text-align: left;
|
|
|
|
|
|
line-height: 1.4;
|
|
|
|
|
|
margin-bottom: 50px;
|
|
|
|
|
|
opacity: 0.3;
|
2026-03-05 16:10:27 +01:00
|
|
|
|
transform: scale(0.75) translateX(4rem);
|
2026-02-19 18:54:25 +01:00
|
|
|
|
transform-origin: left center;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.expertise-item.active {
|
|
|
|
|
|
opacity: 1;
|
2026-03-05 16:10:27 +01:00
|
|
|
|
transform: scale(1) translateX(0);
|
|
|
|
|
|
margin-left: 0;
|
2026-02-19 18:54:25 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Mobile (≤ 700px) */
|
2026-03-10 18:55:37 +01:00
|
|
|
|
@media (max-width: 700px) {
|
2026-02-19 18:54:25 +01:00
|
|
|
|
.expertise-title {
|
|
|
|
|
|
grid-area: 5/4 / span 1 / span 7;
|
|
|
|
|
|
font-size: var(--font-size-title-main-mobile);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.expertise-text-wrapper {
|
|
|
|
|
|
grid-area: 6/4 / span 8 / span 14;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.expertise-item {
|
|
|
|
|
|
font-size: var(--font-size-expertise-mobile);
|
2026-03-12 15:05:25 +01:00
|
|
|
|
transform: scale(0.75) translateX(2rem);
|
2026-02-19 18:54:25 +01:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Tablet (701–912px) */
|
2026-03-10 18:55:37 +01:00
|
|
|
|
@media (min-width: 701px) and (max-width: 912px) {
|
2026-02-19 18:54:25 +01:00
|
|
|
|
.expertise-title {
|
|
|
|
|
|
grid-area: 5/6 / span 4 / span 12;
|
|
|
|
|
|
font-size: var(--font-size-title-main-tablet);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.expertise-text-wrapper {
|
|
|
|
|
|
grid-area: 8/6 / span 8 / span 10;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.expertise-item {
|
|
|
|
|
|
font-size: var(--font-size-expertise-tablet);
|
|
|
|
|
|
}
|
Migration vers architecture Svelte + Kirby inspirée de design-to-pack
- Mise en place de Svelte 4 avec Vite pour le frontend (SPA)
- Simplification des templates PHP (header/footer minimalistes)
- Création de templates JSON pour API (home, about, expertise, portfolio, jouer, game, blog, article, project)
- Ajout d'un controller de site pour définir genericData globalement
- Structure des stores Svelte (page, navigation, locale, site)
- Router avec navaid pour navigation SPA et interception des liens
- Composants layout (Header, Footer, Cursor) et vues de base
- Build Vite vers assets/dist/ (index.js/css)
- Header PHP détecte assets/dist pour basculer dev/prod
Architecture fonctionnelle de base établie, à améliorer et compléter.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-06 16:30:15 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-19 18:54:25 +01:00
|
|
|
|
/* Reduced motion */
|
|
|
|
|
|
@media (prefers-reduced-motion: reduce) {
|
|
|
|
|
|
.expertise-text,
|
|
|
|
|
|
.expertise-item {
|
|
|
|
|
|
transition: none;
|
|
|
|
|
|
}
|
Migration vers architecture Svelte + Kirby inspirée de design-to-pack
- Mise en place de Svelte 4 avec Vite pour le frontend (SPA)
- Simplification des templates PHP (header/footer minimalistes)
- Création de templates JSON pour API (home, about, expertise, portfolio, jouer, game, blog, article, project)
- Ajout d'un controller de site pour définir genericData globalement
- Structure des stores Svelte (page, navigation, locale, site)
- Router avec navaid pour navigation SPA et interception des liens
- Composants layout (Header, Footer, Cursor) et vues de base
- Build Vite vers assets/dist/ (index.js/css)
- Header PHP détecte assets/dist pour basculer dev/prod
Architecture fonctionnelle de base établie, à améliorer et compléter.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-06 16:30:15 +01:00
|
|
|
|
}
|
|
|
|
|
|
</style>
|