- App.svelte : classe active sur la slide courante - Portfolio : flèches PNG custom, <output> pour le compteur, transition gallery mobile avec délai à l'entrée seulement - fonts.css : @font-face Danzza Light + classe .font-face-danzza-light corrigée Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
504 lines
14 KiB
Svelte
504 lines
14 KiB
Svelte
<script>
|
||
import { onMount } from 'svelte'
|
||
import { slides } from '@state/slides.svelte'
|
||
import { navigation } from '@state/navigation.svelte'
|
||
import { createScrollNav } from '@composables/useScrollNav.svelte.js'
|
||
import GalleryAnimation from '@components/ui/GalleryAnimation.svelte'
|
||
import ResponsivePicture from '@components/ui/ResponsivePicture.svelte'
|
||
|
||
let { data } = $props()
|
||
|
||
// --- State ---
|
||
let currentIndex = $state(0)
|
||
let sectionEl = $state(null)
|
||
|
||
// --- Derived ---
|
||
const isActive = $derived(slides.active?.id === 'portfolio')
|
||
const projects = $derived(data?.projects ?? [])
|
||
const backgroundImage = $derived(data?.backgroundImage ?? null)
|
||
const currentProject = $derived(projects[currentIndex] ?? null)
|
||
|
||
$effect(() => {
|
||
if (isActive) navigation.setScrolled(currentIndex > 0)
|
||
})
|
||
|
||
// 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,
|
||
onNavigate: (dir) => {
|
||
const next = dir === 'down'
|
||
? Math.min(currentIndex + 1, projects.length - 1)
|
||
: Math.max(currentIndex - 1, 0)
|
||
if (next === currentIndex) return false
|
||
currentIndex = next
|
||
setAnchor(next)
|
||
},
|
||
})
|
||
|
||
// --- onMount ---
|
||
onMount(() => {
|
||
sectionEl?.addEventListener('wheel', nav.onWheel, { passive: false })
|
||
window.addEventListener('keydown', nav.onKeyDown)
|
||
|
||
return () => {
|
||
sectionEl?.removeEventListener('wheel', nav.onWheel)
|
||
window.removeEventListener('keydown', nav.onKeyDown)
|
||
nav.destroy()
|
||
}
|
||
})
|
||
|
||
// --- 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) {
|
||
wasActive = true
|
||
} else if (wasActive) {
|
||
nav.reset()
|
||
currentIndex = 0
|
||
clearAnchor()
|
||
wasActive = false
|
||
}
|
||
})
|
||
</script>
|
||
|
||
<div class="portfolio-gallery mobile-only" aria-hidden="true">
|
||
<GalleryAnimation images={currentProject.imagesGallery} backgroundColor={currentProject.galleryBackgroundColor} />
|
||
</div>
|
||
|
||
<section
|
||
class="portfolio golden-grid"
|
||
style={backgroundImage ? `--background-image: url('${backgroundImage}')` : ''}
|
||
bind:this={sectionEl}
|
||
ontouchstart={nav.onTouchStart}
|
||
ontouchend={nav.onTouchEnd}
|
||
aria-label="Portfolio"
|
||
>
|
||
<!-- Decorative vertical lines -->
|
||
<div class="vertical-line-start" aria-hidden="true"></div>
|
||
<div class="vertical-line vertical-line-col8" aria-hidden="true"></div>
|
||
<div class="vertical-line-center" aria-hidden="true"></div>
|
||
<div class="vertical-line vertical-line-col14" aria-hidden="true"></div>
|
||
<div class="vertical-line-end" aria-hidden="true"></div>
|
||
|
||
{#if currentProject}
|
||
<!-- Galerie animation (gauche desktop / plein écran mobile) -->
|
||
<div class="portfolio-gallery desktop-only" aria-hidden="true">
|
||
<GalleryAnimation images={currentProject.imagesGallery} backgroundColor={currentProject.galleryBackgroundColor} />
|
||
</div>
|
||
|
||
<!-- Mockup device (centre) -->
|
||
<div class="portfolio-mockup">
|
||
<ResponsivePicture
|
||
src={currentProject.mockup}
|
||
srcset={currentProject.mockupSrcset}
|
||
webp={currentProject.mockupWebp}
|
||
sizes="(max-width: 700px) 90vw, 25vw"
|
||
alt={currentProject.title}
|
||
cls="portfolio-mockup-img"
|
||
/>
|
||
</div>
|
||
<img class="content-background mobile-only" src="/assets/img/BG GAME MOBILE.408a3a253492f65d39f8.png" alt="">
|
||
|
||
<!-- Infos projet (droite) -->
|
||
<div class="portfolio-text" aria-live="polite">
|
||
<h2>{currentProject.title}</h2>
|
||
<h3 class="portfolio-catchphrase gradient-blue">{@html currentProject.catchPhrase}</h3>
|
||
<div class="portfolio-description">{@html currentProject.description}</div>
|
||
<div class="portfolio-keywords">
|
||
{#each currentProject.keywords as kw}
|
||
<p><strong>{kw.label} :</strong> {kw.text}</p>
|
||
{/each}
|
||
</div>
|
||
<div class="portfolio-links">
|
||
{#each currentProject.externalLinks as link}
|
||
<a href={link.url} target="_blank" rel="noopener noreferrer" class="button earth-icon">{link.label}</a>
|
||
{/each}
|
||
</div>
|
||
</div>
|
||
{/if}
|
||
|
||
<!-- Sidebar navigation -->
|
||
<nav class="portfolio-nav" aria-label="Projets">
|
||
<ul role="list">
|
||
{#each projects as project, i}
|
||
<li class="portfolio-nav-item" class:active={i === currentIndex}>
|
||
<button
|
||
aria-current={i === currentIndex ? 'true' : undefined}
|
||
aria-label={project.title}
|
||
onclick={() => { currentIndex = i; setAnchor(i) }}
|
||
>
|
||
{#if project.thumbnail?.length}
|
||
<img src={project.thumbnail} alt="" />
|
||
{/if}
|
||
<span class="portfolio-nav-number" aria-hidden="true">{String(i + 1).padStart(2, '0')}</span>
|
||
</button>
|
||
</li>
|
||
{/each}
|
||
</ul>
|
||
</nav>
|
||
|
||
<!-- Arrows + counter -->
|
||
<div class="portfolio-arrows font-face-danzza" aria-label="Navigation projets">
|
||
<button
|
||
class="portfolio-arrow portfolio-arrow--up"
|
||
aria-label="Projet précédent"
|
||
disabled={currentIndex === 0}
|
||
onclick={() => { if (currentIndex > 0) { currentIndex--; setAnchor(currentIndex) } }}
|
||
></button>
|
||
<button
|
||
class="portfolio-arrow portfolio-arrow--down"
|
||
aria-label="Projet suivant"
|
||
disabled={currentIndex >= projects.length - 1}
|
||
onclick={() => { if (currentIndex < projects.length - 1) { currentIndex++; setAnchor(currentIndex) } }}
|
||
></button>
|
||
<output class="portfolio-counter">{String(currentIndex + 1).padStart(2, '0')}/{String(projects.length).padStart(2, '0')}</output>
|
||
</div>
|
||
</section>
|
||
|
||
<style>
|
||
.portfolio {
|
||
background-image: var(--background-image, none);
|
||
background-position: center;
|
||
background-repeat: no-repeat;
|
||
background-size: cover;
|
||
}
|
||
|
||
/* 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;
|
||
}
|
||
|
||
/* Desktop layout */
|
||
.portfolio-gallery {
|
||
grid-area: 1/1 / span 20 / span 7;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.portfolio-mockup {
|
||
grid-area: 6/7 / span 10 / span 4;
|
||
z-index: var(--z-content);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.portfolio-mockup :global(picture),
|
||
.portfolio-mockup :global(.portfolio-mockup-img) {
|
||
width: 100%;
|
||
height: 100%;
|
||
object-fit: contain;
|
||
}
|
||
|
||
.portfolio-text {
|
||
grid-area: 7/11 / span 6 / span 5;
|
||
z-index: var(--z-content);
|
||
text-align: left;
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: center;
|
||
gap: 0.75rem;
|
||
color: var(--color-text);
|
||
}
|
||
|
||
.portfolio-text h2 {
|
||
font-size: var(--font-size-title-main);
|
||
font-weight: 700;
|
||
text-transform: uppercase;
|
||
line-height: 1.1;
|
||
}
|
||
|
||
.portfolio-catchphrase {
|
||
font-family: "Danzza Medium", sans-serif;
|
||
font-size: var(--font-size-subtitle);
|
||
font-weight: 600;
|
||
color: var(--color-primary);
|
||
}
|
||
|
||
.portfolio-description {
|
||
font-size: var(--font-size-subtitle);
|
||
line-height: 1.5;
|
||
opacity: 0.8;
|
||
}
|
||
|
||
.portfolio-keywords {
|
||
font-size: var(--font-size-caption);
|
||
opacity: 0.6;
|
||
}
|
||
|
||
.portfolio-keywords p {
|
||
margin: 0;
|
||
}
|
||
|
||
.portfolio-links {
|
||
display: flex;
|
||
gap: 0.75rem;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
/* Sidebar navigation */
|
||
.portfolio-nav ul {
|
||
list-style: none;
|
||
display: contents;
|
||
}
|
||
|
||
.portfolio-nav {
|
||
grid-area: 4/17 / span 14 / span 4;
|
||
margin-right: 7vw;
|
||
z-index: var(--z-content);
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: flex-end;
|
||
justify-content: center;
|
||
gap: 1rem;
|
||
}
|
||
|
||
.portfolio-nav-item:not(:last-child) {
|
||
margin-bottom: 1rem;
|
||
}
|
||
|
||
.portfolio-nav-item button {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: 0.5rem;
|
||
background: none;
|
||
border: none;
|
||
cursor: pointer;
|
||
transform: scale(0.85);
|
||
opacity: 0.5;
|
||
transition: transform 0.6s var(--ease-standard), opacity 0.6s var(--ease-standard);
|
||
}
|
||
|
||
.portfolio-nav-item:hover button {
|
||
transform: scale(1.25);
|
||
}
|
||
|
||
.portfolio-nav-item.active button {
|
||
transform: scale(1.5) translateX(-20%);
|
||
opacity: 1;
|
||
}
|
||
|
||
.portfolio-nav-number {
|
||
color: var(--color-text);
|
||
font-size: .5rem;
|
||
}
|
||
|
||
.portfolio-nav-item img {
|
||
width: 48px;
|
||
height: 48px;
|
||
object-fit: cover;
|
||
border-radius: 8px;
|
||
}
|
||
|
||
/* Arrows + counter */
|
||
.portfolio-arrows {
|
||
grid-area: 18/16 / span 2 / span 3;
|
||
z-index: var(--z-content);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: flex-end;
|
||
gap: 4px;
|
||
color: white;
|
||
font-size: var(--font-size-caption);
|
||
letter-spacing: 1px;
|
||
}
|
||
|
||
.portfolio-arrow {
|
||
width: 13px;
|
||
min-width: 13px;
|
||
height: 13px;
|
||
background: none;
|
||
border: none;
|
||
cursor: pointer;
|
||
background-position: center;
|
||
background-size: contain;
|
||
background-repeat: no-repeat;
|
||
padding: 0;
|
||
opacity: 0.8;
|
||
transition: opacity 0.2s;
|
||
}
|
||
|
||
.portfolio-arrow:disabled {
|
||
opacity: 0.3;
|
||
cursor: default;
|
||
}
|
||
|
||
.portfolio-arrow--up {
|
||
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAsAAAARCAYAAAAL4VbbAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAACxSURBVChT7dExDgFBFMbxF4RKolC5ABGRuIIjqJQu4ATiDE6hcQKNhsJGi4YLqBQ6jfF/k53Js8YNfMkvb9/Mm8lmV5xzRTVsMDZrXkm+U8EQA9/ZFE7PYHND3Lc3T9DC1nciVxyg611dCKfq0EyhvWaE8LyAv3mFB/pYogFNO689dPDS4Qt2OOKOKjShnrHGPryG1YRmjo+91Kf7mf+wTWq4nNfwJ2NSw09kOPkuRuQNVDyfOxATXhEAAAAASUVORK5CYII=");
|
||
transform: rotate(180deg);
|
||
margin-right: 2px;
|
||
}
|
||
|
||
.portfolio-arrow--down {
|
||
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAsAAAARCAYAAAAL4VbbAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAACxSURBVChT7dExDgFBFMbxF4RKolC5ABGRuIIjqJQu4ATiDE6hcQKNhsJGi4YLqBQ6jfF/k53Js8YNfMkvb9/Mm8lmV5xzRTVsMDZrXkm+U8EQA9/ZFE7PYHND3Lc3T9DC1nciVxyg611dCKfq0EyhvWaE8LyAv3mFB/pYogFNO689dPDS4Qt2OOKOKjShnrHGPryG1YRmjo+91Kf7mf+wTWq4nNfwJ2NSw09kOPkuRuQNVDyfOxATXhEAAAAASUVORK5CYII=");
|
||
margin-right: 5px;
|
||
}
|
||
|
||
.portfolio-counter {
|
||
white-space: nowrap;
|
||
}
|
||
|
||
/* Mobile (≤ 700px) */
|
||
@media (max-width: 700px) {
|
||
|
||
.portfolio {
|
||
background: transparent;
|
||
}
|
||
|
||
/* Gallery over background, under content */
|
||
.portfolio-gallery.mobile-only {
|
||
position: fixed;
|
||
opacity: 0.8;
|
||
z-index: 1;
|
||
transform: translateX(0vw);
|
||
width: 100vw;
|
||
transition: all .3s ease-in-out;
|
||
}
|
||
|
||
:global(.slide.active .portfolio-gallery.mobile-only) {
|
||
transform: translateX(-10vw);
|
||
width: 120vw;
|
||
transition: all .3s ease-in-out .8s;
|
||
}
|
||
|
||
.content-background {
|
||
position: fixed;
|
||
bottom: 0;
|
||
width: 100vw;
|
||
height: auto;
|
||
z-index: 5;
|
||
}
|
||
|
||
/* Mockup — centered top, behind text */
|
||
.portfolio-mockup {
|
||
grid-area: 4/4 / span 8 / span 14;
|
||
z-index: var(--z-content);
|
||
}
|
||
|
||
.portfolio-mockup :global(.portfolio-mockup-img) {
|
||
height: auto;
|
||
}
|
||
|
||
/* Text — over mockup, centered */
|
||
.portfolio-text {
|
||
grid-area: 8/4/span 8/span 14;
|
||
z-index: var(--z-content);
|
||
gap: .5rem;
|
||
text-align: center;
|
||
}
|
||
|
||
.portfolio-text h2 {
|
||
font-family: "Danzza Bold";
|
||
font-size: var(--font-size-title-section-mobile);
|
||
}
|
||
|
||
.portfolio-catchphrase {
|
||
font-size: var(--font-size-subtitle-mobile);
|
||
}
|
||
|
||
.portfolio-description {
|
||
font-family: "Danzza Light", sans-serif;
|
||
font-size: var(--font-size-paragraph-mobile);
|
||
font-weight: 600;
|
||
line-height: 1.34;
|
||
}
|
||
|
||
/* Hide keywords on mobile */
|
||
.portfolio-keywords {
|
||
display: none;
|
||
}
|
||
|
||
.portfolio-links {
|
||
margin-top: .5rem;
|
||
justify-content: center;
|
||
}
|
||
|
||
/* Nav thumbnails — horizontal, compact */
|
||
.portfolio-nav {
|
||
grid-area: 17/4 / span 1 / span 14;
|
||
flex-direction: row;
|
||
justify-content: center;
|
||
padding-right: 0;
|
||
height: 75px;
|
||
align-items: center;
|
||
overflow: hidden;
|
||
margin-right: 0;
|
||
}
|
||
|
||
.portfolio-nav-item:not(:last-child) {
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
.portfolio-nav-item button {
|
||
flex-direction: column;
|
||
align-items: center;
|
||
}
|
||
|
||
.portfolio-nav-item.active button {
|
||
transform: scale(1.15);
|
||
}
|
||
|
||
.portfolio-nav-item:hover button {
|
||
transform: scale(1.1);
|
||
}
|
||
|
||
.portfolio-nav-item img {
|
||
width: 3.75rem;
|
||
height: 3.75rem;
|
||
}
|
||
|
||
.portfolio-nav-number {
|
||
display: none;
|
||
}
|
||
|
||
/* Arrows — bottom right */
|
||
.portfolio-arrows {
|
||
grid-area: 18/15 / span 1 / span 3;
|
||
font-size: var(--font-size-caption-mobile, 11px);
|
||
}
|
||
|
||
.portfolio-counter {
|
||
font-size: 0.8125rem;
|
||
}
|
||
}
|
||
|
||
/* Tablet (701px–912px) */
|
||
@media (min-width: 701px) and (max-width: 912px) {
|
||
.portfolio-text h2 {
|
||
font-size: var(--font-size-title-main-tablet);
|
||
}
|
||
}
|
||
|
||
/* Reduced motion */
|
||
@media (prefers-reduced-motion: reduce) {
|
||
.portfolio-nav-item {
|
||
transition: none;
|
||
}
|
||
}
|
||
</style>
|