feat(marketing): section interactive 'Comment ça marche' (réacteur DictIA cyclant 6 features)

Ajoute une nouvelle section interactive sous les 6 fonctionnalités
(préservées intégralement) reproduisant le composant React
dictai-narrative.tsx en CSS pur + Alpine.js — sans Framer Motion ni
autre lib JS.

- Réacteur central holographique : 3 anneaux concentriques rotatifs
  (15 s / 22 s / 30 s) + 8 particules orbitales (cyan/blue/fuchsia)
  + wordmark DictIA glow pulsant
- Auto-cycle Alpine.js entre 6 features (1.6 s) avec pause au
  hover/focus et reprise au leave/blur
- Panneau feature active avec aria-live='polite' pour annonce
  lecteur d'écran (Transcription · Diarisation · 99+ langues ·
  Exports · Utilisateurs illimités · Partage & Classement)
- Card 'IA intégrée Mistral 7B LOCAL' avec 3 bullets souveraineté
- Spec list cliquable / hover déclenchant feature dans réacteur
- Layout responsive grid 2 cols desktop, stack mobile
- prefers-reduced-motion désactive rings + orbites + auto-cycle
- Position : APRÈS '6 fonctionnalités', AVANT 'Intégrations'
- Sub-nav reste à 4 ancres (sous-partie visuelle de Fonctionnalités)
- Tests : nouveau test_fonctionnalites_how_it_works_reactor_section
  valide structure, contenu canonique, a11y et Alpine bindings

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Allison
2026-04-29 09:09:40 -04:00
parent e06cba2123
commit 03f6e56f04
3 changed files with 295 additions and 0 deletions

View File

@@ -984,6 +984,9 @@
.min-h-\[110px\] {
min-height: 110px;
}
.min-h-\[480px\] {
min-height: 480px;
}
.min-h-\[calc\(100vh-62px\)\] {
min-height: calc(100vh - 62px);
}
@@ -1140,6 +1143,9 @@
.max-w-\[300px\] {
max-width: 300px;
}
.max-w-\[320px\] {
max-width: 320px;
}
.max-w-\[820px\] {
max-width: 820px;
}
@@ -1242,6 +1248,10 @@
--tw-translate-y: calc(var(--spacing) * 0);
translate: var(--tw-translate-x) var(--tw-translate-y);
}
.translate-y-1 {
--tw-translate-y: calc(var(--spacing) * 1);
translate: var(--tw-translate-x) var(--tw-translate-y);
}
.translate-y-2 {
--tw-translate-y: calc(var(--spacing) * 2);
translate: var(--tw-translate-x) var(--tw-translate-y);
@@ -1916,6 +1926,12 @@
border-color: color-mix(in oklab, var(--color-white) 8%, transparent);
}
}
.border-white\/\[0\.10\] {
border-color: color-mix(in srgb, #fff 10%, transparent);
@supports (color: color-mix(in lab, red, red)) {
border-color: color-mix(in oklab, var(--color-white) 10%, transparent);
}
}
.border-white\/\[0\.12\] {
border-color: color-mix(in srgb, #fff 12%, transparent);
@supports (color: color-mix(in lab, red, red)) {
@@ -2950,6 +2966,10 @@
--tw-tracking: 0.10em;
letter-spacing: 0.10em;
}
.tracking-\[0\.14em\] {
--tw-tracking: 0.14em;
letter-spacing: 0.14em;
}
.tracking-\[0\.16em\] {
--tw-tracking: 0.16em;
letter-spacing: 0.16em;
@@ -5064,6 +5084,12 @@
text-align: right;
}
}
.sm\:text-6xl {
@media (width >= 40rem) {
font-size: var(--text-6xl);
line-height: var(--tw-leading, var(--text-6xl--line-height));
}
}
.sm\:text-base {
@media (width >= 40rem) {
font-size: var(--text-base);
@@ -5457,6 +5483,11 @@
grid-template-columns: 1fr 240px;
}
}
.lg\:grid-cols-\[1fr_minmax\(0\,360px\)\] {
@media (width >= 64rem) {
grid-template-columns: 1fr minmax(0,360px);
}
}
.lg\:grid-cols-\[minmax\(0\,1fr\)_minmax\(0\,420px\)\] {
@media (width >= 64rem) {
grid-template-columns: minmax(0,1fr) minmax(0,420px);

View File

@@ -249,6 +249,221 @@
</div>
</section>
{# ===== COMMENT ÇA MARCHE — RÉACTEUR INTERACTIF =====
Sous-partie visuelle de la section Fonctionnalités (sub-nav reste à 4 ancres).
Reproduit dictai-narrative.tsx (Website-Sanity) en CSS pur + Alpine.js.
#}
<section class="bg-brand-bg py-20" aria-labelledby="how-it-works-title">
<style>
/* Anneaux concentriques rotatifs (réacteur DictIA) */
.reactor-ring { position: absolute; border-radius: 9999px; pointer-events: none; }
.ring-outer { width: 420px; height: 420px; border: 1px solid rgba(37,99,235,0.25); animation: ring-rotate-cw 30s linear infinite; }
.ring-mid { width: 300px; height: 300px; border: 1px solid rgba(6,182,212,0.30); animation: ring-rotate-ccw 22s linear infinite; }
.ring-inner { width: 180px; height: 180px; border: 1px solid rgba(192,38,211,0.35); animation: ring-rotate-cw 15s linear infinite; }
@keyframes ring-rotate-cw { from { transform: rotate(0); } to { transform: rotate(360deg); } }
@keyframes ring-rotate-ccw { from { transform: rotate(0); } to { transform: rotate(-360deg); } }
/* Particules orbitales — 8 dots qui tournent autour du wordmark */
.reactor-orbit-host { position: absolute; left: 50%; top: 50%; width: 1px; height: 1px; }
.orbit { position: absolute; left: 0; top: 0; border-radius: 9999px; transform-origin: 0 0; }
.orbit-1 { width: 8px; height: 8px; background: #2563eb; animation: orbit-spin 12s linear infinite; --r: 210px; }
.orbit-2 { width: 6px; height: 6px; background: #06b6d4; animation: orbit-spin 18s linear infinite reverse; --r: 150px; }
.orbit-3 { width: 6px; height: 6px; background: #c026d3; animation: orbit-spin 9s linear infinite; --r: 90px; }
.orbit-4 { width: 5px; height: 5px; background: #06b6d4; animation: orbit-spin 14s linear infinite reverse; --r: 210px; animation-delay: -3.5s; }
.orbit-5 { width: 7px; height: 7px; background: #c026d3; animation: orbit-spin 20s linear infinite; --r: 150px; animation-delay: -5s; }
.orbit-6 { width: 5px; height: 5px; background: #2563eb; animation: orbit-spin 11s linear infinite reverse; --r: 90px; animation-delay: -2s; }
.orbit-7 { width: 4px; height: 4px; background: #06b6d4; animation: orbit-spin 16s linear infinite; --r: 210px; animation-delay: -8s; }
.orbit-8 { width: 5px; height: 5px; background: #c026d3; animation: orbit-spin 13s linear infinite reverse; --r: 150px; animation-delay: -1s; }
@keyframes orbit-spin {
from { transform: rotate(0deg) translateX(var(--r)) rotate(0deg); }
to { transform: rotate(360deg) translateX(var(--r)) rotate(-360deg); }
}
/* Glow pulsant sous le wordmark central */
@keyframes reactor-glow-pulse {
0%, 100% { opacity: 0.55; transform: scale(1); }
50% { opacity: 0.85; transform: scale(1.08); }
}
.reactor-glow { animation: reactor-glow-pulse 3.2s ease-in-out infinite; }
/* Spec list item état actif */
.feature-list-item { transition: background-color 200ms ease-out, color 150ms ease-out, border-color 150ms ease-out; }
.feature-list-item.is-active { background: rgba(37,99,235,0.10); border-left-color: #2563eb; color: #060d1a; }
@media (prefers-reduced-motion: reduce) {
.ring-outer, .ring-mid, .ring-inner,
.orbit, .reactor-glow { animation: none; }
}
/* Mobile: tighter ring sizes pour rester dans le cadre */
@media (max-width: 640px) {
.ring-outer { width: 320px; height: 320px; }
.ring-mid { width: 220px; height: 220px; }
.ring-inner { width: 140px; height: 140px; }
.orbit-1, .orbit-4, .orbit-7 { --r: 160px; }
.orbit-2, .orbit-5, .orbit-8 { --r: 110px; }
.orbit-3, .orbit-6 { --r: 70px; }
}
</style>
<div class="max-w-[1200px] mx-auto px-6">
<div class="text-center max-w-2xl mx-auto mb-12">
<p class="eyebrow grad-text mb-3 inline-flex items-center gap-2 justify-center">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="w-4 h-4" aria-hidden="true">
<circle cx="12" cy="12" r="3"/>
<path d="M19.4 15a1.7 1.7 0 0 0 .3 1.9l.1.1a2 2 0 1 1-2.8 2.8l-.1-.1a1.7 1.7 0 0 0-1.9-.3 1.7 1.7 0 0 0-1 1.5V21a2 2 0 1 1-4 0v-.1a1.7 1.7 0 0 0-1-1.5 1.7 1.7 0 0 0-1.9.3l-.1.1a2 2 0 1 1-2.8-2.8l.1-.1a1.7 1.7 0 0 0 .3-1.9 1.7 1.7 0 0 0-1.5-1H3a2 2 0 1 1 0-4h.1a1.7 1.7 0 0 0 1.5-1 1.7 1.7 0 0 0-.3-1.9l-.1-.1a2 2 0 1 1 2.8-2.8l.1.1a1.7 1.7 0 0 0 1.9.3h.1a1.7 1.7 0 0 0 1-1.5V3a2 2 0 1 1 4 0v.1a1.7 1.7 0 0 0 1 1.5 1.7 1.7 0 0 0 1.9-.3l.1-.1a2 2 0 1 1 2.8 2.8l-.1.1a1.7 1.7 0 0 0-.3 1.9v.1a1.7 1.7 0 0 0 1.5 1H21a2 2 0 1 1 0 4h-.1a1.7 1.7 0 0 0-1.5 1z"/>
</svg>
COMMENT ÇA MARCHE
</p>
<h2 id="how-it-works-title" class="text-[clamp(2rem,3vw,2.75rem)] font-black text-brand-navy mb-3">
Du fichier au résumé&nbsp;<span class="grad-text">en temps réel</span>
</h2>
<p class="text-base text-brand-navy/70">
Survolez une fonctionnalité pour voir la machine en action. Glissez pour calculer votre gain de productivité.
</p>
</div>
<div x-data='{
features: ["Transcription", "Diarisation", "99+ langues", "Exports", "Utilisateurs illimités", "Partage &amp; Classement"],
details: {
"Transcription": { tag: "WhisperX Large-v3", desc: "STT 95%+ FR-CA" },
"Diarisation": { tag: "pyannote · 8 locuteurs max", desc: "Identification automatique" },
"99+ langues": { tag: "Détection automatique", desc: "FR · EN · ES · ZH · ..." },
"Exports": { tag: "DOCX, SRT, JSON, PDF", desc: "7 formats standards" },
"Utilisateurs illimités": { tag: "Aucun frais par utilisateur", desc: "Volume illimité" },
"Partage &amp; Classement": { tag: "Permissions granulaires", desc: "Tags + dossiers" }
},
active: "Transcription",
isHovered: false,
timer: null,
init() {
if (window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
this.timer = setInterval(() => {
if (!this.isHovered) {
const idx = this.features.indexOf(this.active);
this.active = this.features[(idx + 1) % this.features.length];
}
}, 1600);
},
setActive(name) { this.isHovered = true; this.active = name; },
resumeCycle() { this.isHovered = false; }
}'
x-init="init()"
class="grid lg:grid-cols-[1fr_minmax(0,360px)] gap-8 items-stretch">
{# ── COLONNE GAUCHE : Réacteur central holographique ── #}
<div class="relative bg-brand-navy p-8 rounded overflow-hidden min-h-[480px] flex flex-col items-center justify-center">
{# Backdrop radial #}
<div class="absolute inset-0 pointer-events-none" aria-hidden="true"
style="background: radial-gradient(ellipse 80% 50% at 50% 50%, rgba(37,99,235,0.10) 0%, transparent 70%);"></div>
{# 3 anneaux concentriques rotatifs #}
<div class="absolute inset-0 flex items-center justify-center" aria-hidden="true">
<div class="reactor-ring ring-outer"></div>
<div class="reactor-ring ring-mid"></div>
<div class="reactor-ring ring-inner"></div>
{# 8 particules orbitales #}
<div class="reactor-orbit-host">
<span class="orbit orbit-1"></span>
<span class="orbit orbit-2"></span>
<span class="orbit orbit-3"></span>
<span class="orbit orbit-4"></span>
<span class="orbit orbit-5"></span>
<span class="orbit orbit-6"></span>
<span class="orbit orbit-7"></span>
<span class="orbit orbit-8"></span>
</div>
</div>
{# Auto badge — top right #}
<div class="absolute top-4 right-4 inline-flex items-center gap-1.5 text-xs text-white/80 z-10 font-mono">
<span class="w-2 h-2 rounded-full bg-brand-b2 animate-pulse" aria-hidden="true"></span>
Auto
</div>
{# Centre : DictIA wordmark + glow + feature panel #}
<div class="relative z-10 text-center">
<div class="relative inline-block mb-6">
<div class="absolute inset-0 reactor-glow pointer-events-none" aria-hidden="true"
style="background: radial-gradient(circle at 50% 50%, rgba(6,182,212,0.35) 0%, transparent 65%); filter: blur(24px); transform: scale(1.4);"></div>
<p class="relative font-black text-5xl sm:text-6xl grad-text leading-none tracking-tight">DictIA</p>
</div>
{# Panneau feature active — swap fluide via x-transition #}
<div class="bg-white/[0.06] border border-white/[0.10] rounded p-4 backdrop-blur-sm min-w-[260px] max-w-[320px] mx-auto"
role="status" aria-live="polite" aria-atomic="true">
<template x-for="feat in features" :key="feat">
<div x-show="active === feat"
x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="opacity-0 translate-y-1"
x-transition:enter-end="opacity-100 translate-y-0"
x-transition:leave="transition ease-in duration-150"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0">
<p class="text-[11px] uppercase tracking-[0.18em] text-white/60 mb-1 font-bold" x-text="feat"></p>
<p class="text-sm text-white font-semibold font-mono" x-text="details[feat].tag"></p>
<p class="text-xs text-white/65 mt-1" x-text="details[feat].desc"></p>
</div>
</template>
</div>
</div>
</div>
{# ── COLONNE DROITE : IA intégrée Mistral 7B + spec list cliquable ── #}
<div class="flex flex-col gap-4">
{# Card "IA intégrée Mistral 7B LOCAL" #}
<div class="bg-white border border-brand-border rounded p-5">
<div class="flex items-center gap-2 mb-2 flex-wrap">
<p class="eyebrow text-brand-navy/70">IA intégrée</p>
<span class="px-2 py-0.5 rounded-full bg-brand-b1/10 text-brand-b1 text-[10px] font-bold uppercase tracking-[0.14em] border border-brand-b1/20">Local</span>
</div>
<p class="text-2xl font-black text-brand-navy mb-1">Mistral 7B</p>
<p class="text-sm text-brand-navy/70 mb-4">Résumé · Points d'action · Q&amp;R</p>
<ul class="space-y-2 text-xs text-brand-navy/80" role="list">
<li class="flex items-start gap-2">
{{ icon_check | safe }}
<span>Données hébergées sur VOS serveurs · jamais partagées</span>
</li>
<li class="flex items-start gap-2">
{{ icon_check | safe }}
<span>Zéro connexion OpenAI · Google · Microsoft</span>
</li>
<li class="flex items-start gap-2">
{{ icon_check | safe }}
<span>Inférence hors-ligne · résultats en secondes</span>
</li>
</ul>
</div>
{# Spec list cliquable / hover — déclenche feature dans réacteur #}
<div class="bg-white border border-brand-border rounded p-3">
<p class="eyebrow text-brand-navy/70 px-2 mb-2 mt-1">Fonctions clés</p>
<ul role="list" class="flex flex-col">
<template x-for="feat in features" :key="feat">
<li>
<button type="button"
@mouseenter="setActive(feat)"
@mouseleave="resumeCycle()"
@focus="setActive(feat)"
@blur="resumeCycle()"
@click="setActive(feat)"
:class="active === feat ? 'is-active' : 'border-l-2 border-transparent text-brand-navy/70'"
class="feature-list-item w-full text-left px-3 py-2 text-sm font-semibold border-l-2 hover:text-brand-navy hover:bg-brand-bg focus-visible:outline-2 focus-visible:outline-brand-b1 focus-visible:outline-offset-2 rounded-none"
:aria-pressed="active === feat ? 'true' : 'false'">
<span x-text="feat"></span>
</button>
</li>
</template>
</ul>
</div>
</div>
</div>
</div>
</section>
{# ===== INTÉGRATIONS ===== #}
<section id="integrations" class="bg-brand-bg py-20 scroll-mt-32" aria-labelledby="integrations-title">
<div class="max-w-[1200px] mx-auto px-6">

View File

@@ -131,6 +131,55 @@ def test_fonctionnalites_renders_6_bento_cards():
assert kw in body
def test_fonctionnalites_how_it_works_reactor_section():
"""New 'Comment ça marche' interactive reactor section (post-6-features, pre-integrations).
Validates structure (heading + reactor + spec list + Mistral card), 6 cycling features,
canonical content, and a11y signals (aria-labelledby + aria-live status panel).
"""
client = app.test_client()
body = client.get('/fonctionnalites').data.decode('utf-8')
# Section heading + canonical phrasing
assert 'how-it-works-title' in body, "Missing how-it-works section anchor"
assert 'COMMENT ÇA MARCHE' in body, "Missing canonical eyebrow"
assert 'Du fichier au résumé' in body, "Missing canonical H2 phrasing"
assert 'en temps réel' in body
assert 'Survolez une fonctionnalité pour voir la machine en action' in body
# 6 cycling features (canonical names)
for feat in ['Transcription', 'Diarisation', '99+ langues', 'Exports',
'Utilisateurs illimités', 'Partage &amp; Classement']:
assert feat in body, f"Missing cycling feature: {feat}"
# Reactor visual: rings, orbits, wordmark, Auto badge
assert 'reactor-ring' in body and 'ring-outer' in body and 'ring-mid' in body and 'ring-inner' in body
assert 'orbit orbit-1' in body and 'orbit orbit-8' in body, "Missing 8 orbital particles"
assert '>DictIA<' in body, "Missing reactor centre wordmark"
# Mistral 7B IA intégrée card with 3 canonical bullets
assert 'Mistral 7B' in body
assert 'IA intégrée' in body
assert 'Données hébergées sur VOS serveurs' in body
assert 'Zéro connexion OpenAI' in body
assert 'Inférence hors-ligne' in body
# Alpine reactor data + auto-cycle + hover/focus stop logic
assert "active: \"Transcription\"" in body
assert 'isHovered' in body
assert 'setActive(feat)' in body
assert 'resumeCycle()' in body
# Hover, focus + blur listeners on each list item
assert '@mouseenter="setActive(feat)"' in body
assert '@mouseleave="resumeCycle()"' in body
assert '@focus="setActive(feat)"' in body
assert '@blur="resumeCycle()"' in body
# Accessibility: aria-live status panel, prefers-reduced-motion guard
assert 'aria-live="polite"' in body
assert 'prefers-reduced-motion' in body, "Reduced-motion guard missing"
def test_fonctionnalites_export_formats_section():
client = app.test_client()
body = client.get('/fonctionnalites').data.decode('utf-8')