design : accueil en trois variantes (console, parcours, établi) + couleur par axe

- typo : Bricolage Grotesque (titres) + Atkinson Hyperlegible (texte),
  Syne gardée pour le logo seul ; une seule source de polices (Bunny)
- palettes : teinte propre à chaque axe dans les 4 ambiances ;
  pages d'axe en axis-scope, cartes sans bordure (canon design)
- accueil : console des trois curseurs (tablist clavier), boîte à outils,
  bloc format éditorial + événement + reproduction ; ?v=a|b|c|0
- numérique : carte LPK ; bouquet rendu avec statut et légende
- messages : sans bordure, deux colonnes en grand écran

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Yvv
2026-09-28 10:15:46 +02:00
co-authored by Claude Opus 5.5
parent c3ada04e52
commit e9fce4be8f
29 changed files with 2901 additions and 56 deletions
+492
View File
@@ -0,0 +1,492 @@
<template>
<div class="vc">
<section class="vc-hero">
<HomeShadoks />
<div class="container-content vc-hero-inner">
<h1 class="vc-h1">
{{ heading[0] }} <span>{{ heading[1] }}</span>
</h1>
<p v-if="format" class="vc-lede">{{ format.text }}</p>
</div>
</section>
<section class="container-content vc-bench" aria-label="La boîte à outils">
<!-- Filters -->
<div class="vc-filters">
<div class="vc-chips" role="group" aria-label="Filtrer par axe">
<button
type="button"
class="vc-chip"
:class="{ 'vc-chip--on': filter === 'all' }"
:aria-pressed="filter === 'all'"
@click="filter = 'all'"
>
Tout
</button>
<button
v-for="axis in axes"
:key="axis.key"
type="button"
class="vc-chip"
:class="{ 'vc-chip--on': filter === axis.key }"
:style="{ '--hue': `var(${axis.hue})` }"
:aria-pressed="filter === axis.key"
@click="filter = axis.key"
>
<span class="vc-chip-dot" aria-hidden="true" />
{{ axis.short }}
</button>
</div>
<label class="vc-toggle">
<input v-model="onlyService" type="checkbox">
<span>{{ bouquet?.statuses?.service ?? 'En service' }} seulement</span>
</label>
</div>
<!-- Axis by axis -->
<div v-for="axis in visibleAxes" :key="axis.key" class="vc-axis" :style="{ '--hue': `var(${axis.hue})` }">
<header class="vc-axis-head">
<p class="vc-axis-lever">{{ axis.lever }}</p>
<h2 class="vc-axis-title">
<NuxtLink :to="axis.to">{{ axis.title }}</NuxtLink>
</h2>
<p v-if="axis.promise" class="vc-axis-promise">{{ axis.promise }}</p>
</header>
<div class="vc-grid">
<HomeToolItem
v-for="tool in toolsOf(axis)"
:key="tool.label"
:tool="tool"
class="vc-card"
@action="onAction"
/>
<button
v-if="axis.key === 'economie' && gratewizard"
type="button"
class="vc-card vc-gw"
@click="launch()"
>
<span class="i-lucide-sparkles h-5 w-5" aria-hidden="true" />
<strong>{{ gratewizard.title }}</strong>
<span>{{ gratewizard.subtitle }}</span>
</button>
<p v-if="!toolsOf(axis).length" class="vc-empty">
Tout est en gestation sur cet axe —
<button type="button" class="vc-link" @click="onlyService = false">afficher aussi les outils en gestation</button>.
</p>
</div>
<!-- LPK bouquet, category by category -->
<div v-if="axis.key === 'numerique' && bouquet && lpk" class="vc-bouquet">
<p class="vc-bouquet-head">
<strong>{{ lpk.name }}</strong> {{ lpk.fullName }} — {{ lpk.text }}
</p>
<ul class="vc-legend" aria-label="Légende des statuts">
<li v-for="(label, key) in bouquet.statuses" :key="key">
<span class="vc-dot" :class="`vc-dot--${key}`" aria-hidden="true" />{{ label }}
</li>
</ul>
<div class="vc-cats">
<div v-for="cat in bouquet.categories" v-show="stacksOf(cat).length" :key="cat.label" class="vc-cat">
<p class="vc-cat-label">
<span :class="`i-lucide-${cat.icon}`" class="h-3.5 w-3.5" aria-hidden="true" />
{{ cat.label }}
</p>
<ul>
<li v-for="s in stacksOf(cat)" :key="s.role + s.name">
<span class="vc-dot" :class="`vc-dot--${s.status}`" :title="bouquet.statuses[s.status]" />
<span class="vc-stack-role">{{ s.role }}</span>
<a v-if="s.url && s.name" :href="s.url" target="_blank" rel="noopener" class="vc-stack-name">{{ s.name }}</a>
<span v-else-if="s.name" class="vc-stack-name">{{ s.name }}</span>
<span class="sr-only">— {{ bouquet.statuses[s.status] }}</span>
</li>
</ul>
</div>
</div>
<NuxtLink :to="lpk.to" class="vc-more">
{{ bouquet.title }}
<span class="i-lucide-arrow-right h-4 w-4" aria-hidden="true" />
</NuxtLink>
</div>
</div>
</section>
<section class="vc-section">
<div class="container-content">
<HomeCitations :lines="hero.citations ?? []" class="vc-cites" />
<HomeFormat
:format="format"
:book="book"
:evenement="evenement"
:hero="hero"
:show-header="false"
@open-player="$emit('open-player')"
@open-pdf="$emit('open-pdf')"
/>
</div>
</section>
</div>
</template>
<script setup lang="ts">
import type { AxisKey, HomeAxis, HomeStack } from '~/composables/useHomeData'
const emit = defineEmits<{
'open-player': []
'open-pdf': []
}>()
const { hero, axes, lpk, bouquet, evenement, book, format, gratewizard } = await useHomeData()
const heading = computed<string[]>(() => hero.value.heading ?? [])
const { launch } = useGrateWizard()
const filter = ref<'all' | AxisKey>('all')
const onlyService = ref(false)
const visibleAxes = computed(() =>
filter.value === 'all' ? axes.value : axes.value.filter(a => a.key === filter.value))
function toolsOf(axis: HomeAxis) {
return onlyService.value ? axis.items.filter(t => !t.gestation) : axis.items
}
function stacksOf(cat: { items: (HomeStack | string)[] }) {
const items = cat.items.filter((i): i is HomeStack => typeof i !== 'string')
return onlyService.value ? items.filter(i => i.status === 'service') : items
}
function onAction(id: string) {
if (id === 'open-player') emit('open-player')
else if (id === 'open-pdf') emit('open-pdf')
}
</script>
<style scoped>
.vc-hero {
position: relative;
overflow: hidden;
padding: clamp(2.5rem, 6vw, 4.5rem) 1rem clamp(1.5rem, 3vw, 2.5rem);
}
.vc-hero-inner {
position: relative;
z-index: 1;
}
.vc-h1 {
margin: 0;
max-width: 20ch;
font-family: var(--font-display);
font-weight: 800;
font-size: clamp(2.2rem, 5.6vw, 4rem);
letter-spacing: -0.035em;
line-height: 1;
color: hsl(var(--color-text));
}
.vc-h1 span {
font-weight: 400;
color: hsl(var(--color-text) / 0.55);
}
.vc-lede {
margin: 1.25rem 0 0;
max-width: 40rem;
font-size: 1.1rem;
line-height: 1.5;
color: hsl(var(--color-text) / 0.75);
}
.vc-bench {
padding: 0 1rem;
}
/* Filters */
.vc-filters {
position: sticky;
top: var(--header-height);
z-index: 5;
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: 0.75rem;
margin: 0 -1rem 1.5rem;
padding: 0.75rem 1rem;
background: hsl(var(--color-bg) / 0.92);
backdrop-filter: blur(10px);
}
.vc-chips {
display: flex;
flex-wrap: wrap;
gap: 0.35rem;
}
.vc-chip {
--hue: var(--color-text);
display: inline-flex;
align-items: center;
gap: 0.45rem;
min-height: 2.25rem;
padding: 0.35rem 0.95rem;
border-radius: 20px;
font-family: var(--font-display);
font-weight: 700;
font-size: 0.9rem;
color: hsl(var(--color-text) / 0.75);
background: hsl(var(--color-surface));
transition: transform 0.1s ease, background 0.2s ease;
}
.vc-chip:hover { transform: translateY(-1px); }
.vc-chip:active { transform: translateY(0); }
.vc-chip--on {
color: hsl(var(--color-bg));
background: hsl(var(--hue));
}
.vc-chip-dot {
width: 0.55rem;
height: 0.55rem;
border-radius: 50%;
background: hsl(var(--hue));
}
.vc-chip--on .vc-chip-dot {
background: hsl(var(--color-bg));
}
.vc-toggle {
display: inline-flex;
align-items: center;
gap: 0.5rem;
min-height: 2.25rem;
font-size: 0.85rem;
color: hsl(var(--color-text) / 0.75);
cursor: pointer;
}
.vc-toggle input {
width: 1.1rem;
height: 1.1rem;
accent-color: hsl(var(--color-primary));
}
/* Axis */
.vc-axis + .vc-axis {
margin-top: 3rem;
}
.vc-axis-head {
display: grid;
gap: 0.2rem;
margin-bottom: 1rem;
padding-left: 0.9rem;
box-shadow: inset 3px 0 0 hsl(var(--hue));
}
.vc-axis-lever {
margin: 0;
font-family: var(--font-mono);
font-size: 0.72rem;
color: hsl(var(--hue));
}
.vc-axis-title {
margin: 0;
font-family: var(--font-display);
font-weight: 800;
font-size: clamp(1.4rem, 3vw, 1.9rem);
letter-spacing: -0.025em;
color: hsl(var(--color-text));
}
.vc-axis-title a:hover { color: hsl(var(--hue)); }
.vc-axis-promise {
margin: 0.2rem 0 0;
max-width: 44rem;
font-size: 0.98rem;
line-height: 1.45;
color: hsl(var(--color-text) / 0.72);
}
.vc-grid {
display: grid;
gap: 0.75rem;
}
@media (min-width: 768px) {
.vc-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (min-width: 1024px) {
.vc-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}
.vc-grid > .vc-card {
background: hsl(var(--color-surface));
border-radius: 16px;
padding: 1.1rem;
}
.vc-gw {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 0.3rem;
text-align: left;
font-size: 0.85rem;
color: hsl(var(--color-text) / 0.7);
background: hsl(var(--hue) / 0.1);
transition: transform 0.12s ease;
}
.vc-gw:hover { transform: translateY(-3px); }
.vc-gw > span:first-child { color: hsl(var(--hue)); }
.vc-gw strong {
font-family: var(--font-display);
font-size: 1.05rem;
color: hsl(var(--color-text));
}
.vc-empty {
margin: 0;
font-size: 0.9rem;
color: hsl(var(--color-text) / 0.6);
}
.vc-link {
color: hsl(var(--hue));
text-decoration: underline;
text-underline-offset: 2px;
}
/* Bouquet */
.vc-bouquet {
margin-top: 1rem;
padding: 1.25rem;
border-radius: 16px;
background: hsl(var(--hue) / 0.08);
}
.vc-bouquet-head {
margin: 0;
font-size: 0.95rem;
line-height: 1.45;
color: hsl(var(--color-text) / 0.75);
}
.vc-bouquet-head strong {
font-family: var(--font-display);
font-size: 1.1rem;
color: hsl(var(--hue));
}
.vc-legend {
list-style: none;
display: flex;
flex-wrap: wrap;
gap: 0.3rem 1rem;
margin: 0.75rem 0 1rem;
padding: 0;
font-family: var(--font-mono);
font-size: 0.7rem;
color: hsl(var(--color-text) / 0.6);
}
.vc-legend li {
display: inline-flex;
align-items: center;
gap: 0.4rem;
}
.vc-cats {
columns: 16rem;
column-gap: 1.5rem;
}
.vc-cat {
break-inside: avoid;
margin-bottom: 1.1rem;
}
.vc-cat-label {
display: flex;
align-items: center;
gap: 0.4rem;
margin: 0 0 0.4rem;
font-family: var(--font-display);
font-weight: 700;
font-size: 0.9rem;
color: hsl(var(--color-text));
}
.vc-cat ul {
list-style: none;
margin: 0;
padding: 0;
display: grid;
gap: 0.2rem;
}
.vc-cat li {
display: grid;
grid-template-columns: 0.5rem minmax(0, 1fr);
column-gap: 0.45rem;
align-items: baseline;
font-size: 0.82rem;
line-height: 1.4;
}
.vc-dot {
display: inline-block;
flex-shrink: 0;
width: 0.5rem;
height: 0.5rem;
border-radius: 50%;
align-self: center;
}
.vc-dot--service { background: hsl(var(--hue)); }
.vc-dot--catalogue { box-shadow: inset 0 0 0 1.5px hsl(var(--hue)); }
.vc-dot--avenir { box-shadow: inset 0 0 0 1px hsl(var(--color-text) / 0.3); }
.vc-cat li > .vc-dot { grid-row: 1; }
.vc-cat li > :not(.vc-dot) { grid-column: 2; }
.vc-stack-role { color: hsl(var(--color-text) / 0.6); }
.vc-stack-name {
font-weight: 700;
color: hsl(var(--color-text));
}
a.vc-stack-name:hover { color: hsl(var(--hue)); }
.vc-more {
display: inline-flex;
align-items: center;
gap: 0.35rem;
font-family: var(--font-display);
font-weight: 700;
color: hsl(var(--hue));
}
.vc-section {
padding: clamp(3rem, 7vw, 5rem) 1rem 0;
}
.vc-cites {
margin-bottom: 2.5rem;
}
@media (prefers-reduced-motion: reduce) {
.vc-chip, .vc-gw { transition: none; }
}
</style>