Files
office_translator/frontend/src/components/landing/hero-visual.tsx
2026-08-31 21:48:59 +02:00

1012 lines
45 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client';
import { useEffect, useState, useCallback } from 'react';
import { useReducedMotion } from 'framer-motion';
import {
FileText,
FileSpreadsheet,
Presentation,
FileCheck,
Sparkles,
ArrowRight,
Clock,
Users,
Flame,
Check,
ShieldCheck,
Save,
Undo,
Redo,
Search,
Minus,
Square,
X,
Bold,
Italic,
Underline,
AlignLeft,
AlignCenter,
List,
ListOrdered,
Table as TableIcon,
ChevronDown,
ZoomIn,
Printer,
Maximize2,
} from 'lucide-react';
import { cn } from '@/lib/utils';
/**
* Wordly.art — Hero Visual Authentique Office & PDF
*
* Cette animation recrée fidèlement l'environnement réel d'un logiciel bureautique
* (Microsoft Word, Excel, PowerPoint et Adobe PDF) avec :
* - Le ruban, la barre de titre, la règle et la barre d'état officielles.
* - Une vraie page de document A4 flottante avec du vrai texte lisible (≥ 11px).
* - Une onde dorée de numérisation/traduction en temps réel (FR → EN → ES).
* - Une stabilité géométrique absolue : zéro décalage, zéro saut de mise en page.
*/
type OfficeFormat = 'docx' | 'xlsx' | 'pptx' | 'pdf';
interface RecipeData {
lang: 'FR' | 'EN' | 'ES';
langName: string;
langCodeDoc: string;
docTitle: string;
docSubtitle: string;
category: string;
timeLabel: string;
servingsLabel: string;
heatLabel: string;
ingredientsHeading: string;
ingredients: Array<{ amount: string; name: string }>;
tableTitle: string;
tableHeaders: [string, string, string];
tableValues: [string, string, string];
stepsHeading: string;
steps: Array<{ num: string; title: string; desc: string }>;
excelFormula: string;
excelHeaders: [string, string, string, string];
excelRows: Array<[string, string, string, string]>;
excelTotalLabel: string;
excelTotalVal: string;
pptSlideTitle: string;
pptCard1Title: string;
pptCard1Text: string;
pptCard2Title: string;
pptCard2Text: string;
guaranteeText: string;
}
const RECIPES: RecipeData[] = [
{
lang: 'FR',
langName: 'Français (France)',
langCodeDoc: 'FR-FR',
docTitle: 'Tarte fine aux pommes & cannelle',
docSubtitle: 'Recette traditionnelle • Pâtisserie maison de saison',
category: 'Pâtisserie artisanale • Recette n°04',
timeLabel: '15 min de préparation',
servingsLabel: '4 à 6 personnes',
heatLabel: 'Four chaud 180°C',
ingredientsHeading: 'Ingrédients nécessaires',
ingredients: [
{ amount: '1 rouleau', name: 'Pâte feuilletée pur beurre dorée' },
{ amount: '4 pièces', name: 'Pommes Golden finement tranchées' },
{ amount: '25 g', name: 'Beurre doux fondu tiédi' },
{ amount: '2 sachets', name: 'Sucre vanillé naturel des îles' },
],
tableTitle: 'Paramètres de cuisson au four',
tableHeaders: ['Position four', 'Préparation', 'Cuisson dorée'],
tableValues: ['Mi-hauteur 180°C', '15 minutes', '25 minutes'],
stepsHeading: 'Instructions de préparation',
steps: [
{
num: '01',
title: 'Préparer la pâte',
desc: 'Dérouler la pâte sur la plaque et piquer le fond à la fourchette.',
},
{
num: '02',
title: 'Dresser les pommes',
desc: 'Disposer les lamelles de pommes en rosace harmonieuse et serrée.',
},
{
num: '03',
title: 'Enfourner et dorer',
desc: 'Badigeonner de beurre fondu puis saupoudrer de sucre vanillé.',
},
],
excelFormula: '=SOMME(C3:C6) & " g de garniture"',
excelHeaders: ['Ingrédient', 'Quantité', 'Unité', 'Remarque'],
excelRows: [
['Pâte feuilletée', '1', 'rouleau', 'Pur beurre'],
['Pommes Golden', '4', 'pièces', 'Tranches fines'],
['Beurre doux', '25', 'grammes', 'Fondu tiède'],
['Sucre vanillé', '2', 'sachets', 'Arôme naturel'],
],
excelTotalLabel: 'Temps de cuisson totale',
excelTotalVal: '25 min à 180°C',
pptSlideTitle: 'Recette gourmande : Tarte fine aux pommes',
pptCard1Title: 'Ingrédients clés',
pptCard1Text: 'Pâte feuilletée pur beurre, 4 pommes Golden, beurre doux fondu, sucre vanillé naturel.',
pptCard2Title: 'Cuisson & finition',
pptCard2Text: 'Disposition en rosace serrée, cuisson 25 min à 180°C jusquà dorure parfaite.',
guaranteeText: 'Mise en page originale 100% préservée',
},
{
lang: 'EN',
langName: 'English (United States)',
langCodeDoc: 'EN-US',
docTitle: 'Crisp apple & cinnamon tart',
docSubtitle: 'Traditional recipe • Seasonal homemade pastry',
category: 'Artisanal pastry • Recipe #04',
timeLabel: '15 min preparation time',
servingsLabel: '4 to 6 servings',
heatLabel: 'Hot oven at 180°C',
ingredientsHeading: 'Required ingredients',
ingredients: [
{ amount: '1 sheet', name: 'Golden all-butter puff pastry sheet' },
{ amount: '4 units', name: 'Fresh Golden apples thinly sliced' },
{ amount: '25 g', name: 'Melted unsalted sweet butter' },
{ amount: '2 packs', name: 'Natural island vanilla sugar dust' },
],
tableTitle: 'Oven baking specifications',
tableHeaders: ['Oven position', 'Preparation', 'Golden baking'],
tableValues: ['Middle shelf 180°C', '15 minutes', '25 minutes'],
stepsHeading: 'Preparation instructions',
steps: [
{
num: '01',
title: 'Prepare the crust',
desc: 'Unroll pastry on the tray and prick the base evenly with a fork.',
},
{
num: '02',
title: 'Arrange the fruit',
desc: 'Place the apple slices in an elegant, tight spiral pattern.',
},
{
num: '03',
title: 'Bake to golden',
desc: 'Brush with melted butter and dust evenly with vanilla sugar.',
},
],
excelFormula: '=SUM(C3:C6) & " g filling total"',
excelHeaders: ['Ingredient', 'Quantity', 'Unit', 'Note'],
excelRows: [
['Puff pastry', '1', 'sheet', 'All-butter'],
['Golden apples', '4', 'units', 'Thin slices'],
['Unsalted butter', '25', 'grams', 'Melted warm'],
['Vanilla sugar', '2', 'packs', 'Natural flavor'],
],
excelTotalLabel: 'Total baking duration',
excelTotalVal: '25 min at 180°C',
pptSlideTitle: 'Gourmet Recipe: Crisp Apple Tart',
pptCard1Title: 'Key Ingredients',
pptCard1Text: 'All-butter puff pastry, 4 Golden apples, melted unsalted butter, natural vanilla sugar.',
pptCard2Title: 'Baking & Finish',
pptCard2Text: 'Arrange in tight spiral, bake 25 min at 180°C until crisp and golden brown.',
guaranteeText: 'Original layout 100% preserved',
},
{
lang: 'ES',
langName: 'Español (España)',
langCodeDoc: 'ES-ES',
docTitle: 'Tarta fina de manzana & canela',
docSubtitle: 'Receta tradicional • Repostería casera de otoño',
category: 'Repostería artesanal • Receta n°04',
timeLabel: '15 min de preparación',
servingsLabel: '4 a 6 personas',
heatLabel: 'Horno caliente 180°C',
ingredientsHeading: 'Ingredientes necesarios',
ingredients: [
{ amount: '1 lámina', name: 'Masa de hojaldre dorado con mantequilla' },
{ amount: '4 piezas', name: 'Manzanas Golden finamente laminadas' },
{ amount: '25 g', name: 'Mantequilla dulce derretida templada' },
{ amount: '2 sobres', name: 'Azúcar avainillado natural fino' },
],
tableTitle: 'Parámetros de horneado al horno',
tableHeaders: ['Posición horno', 'Preparación', 'Horneado dorado'],
tableValues: ['Altura media 180°C', '15 minutos', '25 minutes'],
stepsHeading: 'Instrucciones de preparación',
steps: [
{
num: '01',
title: 'Preparar la base',
desc: 'Extender la masa sobre la bandeja y pinchar la base con tenedor.',
},
{
num: '02',
title: 'Colocar la fruta',
desc: 'Disponer las láminas de manzana en una espiral bien apretada.',
},
{
num: '03',
title: 'Hornear y dorar',
desc: 'Pincelar con mantequilla y espolvorear el azúcar avainillado.',
},
],
excelFormula: '=SUMA(C3:C6) & " g de relleno"',
excelHeaders: ['Ingrediente', 'Cantidad', 'Unidad', 'Detalle'],
excelRows: [
['Masa hojaldre', '1', 'lámina', 'Con mantequilla'],
['Manzanas Golden', '4', 'piezas', 'Láminas finas'],
['Mantequilla dulce', '25', 'gramos', 'Derretida tibia'],
['Azúcar vainillado', '2', 'sobres', 'Aroma natural'],
],
excelTotalLabel: 'Tiempo de cocción total',
excelTotalVal: '25 min a 180°C',
pptSlideTitle: 'Receta exquisita: Tarta fina de manzana',
pptCard1Title: 'Ingredientes clave',
pptCard1Text: 'Masa de hojaldre, 4 manzanas Golden, mantequilla dulce derretida, azúcar avainillado.',
pptCard2Title: 'Horneado y acabado',
pptCard2Text: 'Disposición en espiral apretada, hornear 25 min a 180°C hasta que esté dorada.',
guaranteeText: 'Estructura original 100% preservada',
},
];
const FORMAT_CONFIG: Record<
OfficeFormat,
{
ext: string;
label: string;
appTitle: string;
accentColor: string;
headerBg: string;
headerText: string;
statusBg: string;
icon: typeof FileText;
defaultFile: string;
}
> = {
docx: {
ext: '.DOCX',
label: 'Word',
appTitle: 'Word',
accentColor: '#185ABD',
headerBg: 'bg-[#185ABD] dark:bg-[#10438F]',
headerText: 'text-white',
statusBg: 'bg-[#185ABD] dark:bg-[#10438F]',
icon: FileText,
defaultFile: 'Recette_Tarte_Pommes.docx',
},
xlsx: {
ext: '.XLSX',
label: 'Excel',
appTitle: 'Excel',
accentColor: '#107C41',
headerBg: 'bg-[#107C41] dark:bg-[#0B5C30]',
headerText: 'text-white',
statusBg: 'bg-[#107C41] dark:bg-[#0B5C30]',
icon: FileSpreadsheet,
defaultFile: 'Fiche_Technique_Recette.xlsx',
},
pptx: {
ext: '.PPTX',
label: 'PowerPoint',
appTitle: 'PowerPoint',
accentColor: '#C43E1C',
headerBg: 'bg-[#C43E1C] dark:bg-[#992F13]',
headerText: 'text-white',
statusBg: 'bg-[#C43E1C] dark:bg-[#992F13]',
icon: Presentation,
defaultFile: 'Presentation_Recette.pptx',
},
pdf: {
ext: '.PDF',
label: 'PDF Reader',
appTitle: 'Adobe Acrobat Reader',
accentColor: '#E03E2D',
headerBg: 'bg-[#2A2A2A] dark:bg-[#1A1A1A]',
headerText: 'text-white',
statusBg: 'bg-[#2A2A2A] dark:bg-[#1A1A1A]',
icon: FileCheck,
defaultFile: 'Document_Recette_Officiel.pdf',
},
};
const CYCLE_DURATION_MS = 6200; // ~6.2 secondes par cycle
const SCAN_DURATION_MS = 1400; // 1.4s onde dorée
/**
* Rendu intérieur d'un document Word (.docx) avec vraie mise en page A4
*/
function WordDocumentSheet({ data }: { data: RecipeData }) {
return (
<div className="w-full h-full bg-[#FAF8F5] dark:bg-[#1C1C1C] text-[#1A1A1A] dark:text-[#EDEDED] font-sans p-5 sm:p-7 md:p-8 flex flex-col justify-between select-none">
{/* En-tête avec repères de marge Word */}
<div className="border-b border-black/[0.08] dark:border-white/[0.08] pb-3 sm:pb-4">
<div className="flex items-center justify-between gap-2 mb-1.5">
<span className="inline-flex items-center gap-1.5 px-2 py-0.5 rounded text-[11px] font-semibold bg-[#185ABD]/10 text-[#185ABD] dark:text-[#6BA5FF] border border-[#185ABD]/20">
<FileText className="w-3 h-3" />
Style : Document Word Standard
</span>
<span className="text-[11px] font-mono tracking-tight text-[#1A1A1A]/50 dark:text-white/50">
Section 1 Marges normales (2,5 cm)
</span>
</div>
<div className="flex flex-col sm:flex-row sm:items-baseline justify-between gap-1">
<h1 className="font-serif text-[18px] sm:text-[21px] md:text-[23px] font-bold tracking-tight text-[#1A1A1A] dark:text-white leading-tight">
{data.docTitle}
</h1>
<span className="text-[11px] text-[#8B6F47] dark:text-[#C5A17A] font-medium shrink-0">
{data.category}
</span>
</div>
<p className="text-[11.5px] text-[#1A1A1A]/70 dark:text-white/70 italic mt-0.5">
{data.docSubtitle}
</p>
{/* Métadonnées */}
<div className="mt-2.5 flex flex-wrap items-center gap-x-4 gap-y-1 text-[11px] text-[#1A1A1A]/80 dark:text-white/70 font-medium">
<div className="flex items-center gap-1.5">
<Clock className="w-3.5 h-3.5 text-[#185ABD] dark:text-[#6BA5FF]" />
<span>{data.timeLabel}</span>
</div>
<span className="text-black/20 dark:text-white/20"></span>
<div className="flex items-center gap-1.5">
<Users className="w-3.5 h-3.5 text-[#185ABD] dark:text-[#6BA5FF]" />
<span>{data.servingsLabel}</span>
</div>
<span className="text-black/20 dark:text-white/20"></span>
<div className="flex items-center gap-1.5">
<Flame className="w-3.5 h-3.5 text-[#185ABD] dark:text-[#6BA5FF]" />
<span>{data.heatLabel}</span>
</div>
</div>
</div>
{/* Corps 2 colonnes */}
<div className="grid grid-cols-1 md:grid-cols-12 gap-4 sm:gap-6 my-auto py-2">
{/* Colonne Ingrédients + Tableau Word */}
<div className="md:col-span-5 flex flex-col justify-between gap-3">
<div>
<h2 className="text-[11.5px] font-bold uppercase tracking-wider text-[#185ABD] dark:text-[#6BA5FF] mb-2 flex items-center gap-1.5">
<span className="w-1.5 h-1.5 rounded-xs bg-[#185ABD] dark:bg-[#6BA5FF]" />
{data.ingredientsHeading}
</h2>
<ul className="space-y-1.5">
{data.ingredients.map((item, idx) => (
<li
key={idx}
className="flex items-baseline justify-between text-[11.5px] border-b border-black/[0.04] dark:border-white/[0.04] pb-1"
>
<span className="text-[#1A1A1A] dark:text-[#E0E0E0] truncate pr-2">
{item.name}
</span>
<span className="font-mono text-[11px] font-semibold text-[#8B6F47] dark:text-[#C5A17A] shrink-0">
{item.amount}
</span>
</li>
))}
</ul>
</div>
{/* Vrai Tableau Word avec bordures officielles */}
<div className="rounded border border-[#185ABD]/20 dark:border-white/15 overflow-hidden bg-white dark:bg-[#222222]">
<div className="bg-[#185ABD]/10 dark:bg-[#185ABD]/25 px-2.5 py-1 text-[11px] font-bold text-[#185ABD] dark:text-[#8BB8FF] border-b border-[#185ABD]/15">
{data.tableTitle}
</div>
<div className="grid grid-cols-3 divide-x divide-black/[0.06] dark:divide-white/10 text-center p-1.5">
{data.tableHeaders.map((header, hIdx) => (
<div key={hIdx} className="px-1">
<div className="text-[10.5px] font-semibold text-[#8B6F47] dark:text-[#C5A17A] truncate">
{header}
</div>
<div className="text-[11px] font-bold text-[#1A1A1A] dark:text-white mt-0.5 truncate">
{data.tableValues[hIdx]}
</div>
</div>
))}
</div>
</div>
</div>
{/* Colonne Instructions numérotées Word */}
<div className="md:col-span-7 flex flex-col justify-between">
<div>
<h2 className="text-[11.5px] font-bold uppercase tracking-wider text-[#185ABD] dark:text-[#6BA5FF] mb-2 flex items-center gap-1.5">
<span className="w-1.5 h-1.5 rounded-xs bg-[#185ABD] dark:bg-[#6BA5FF]" />
{data.stepsHeading}
</h2>
<div className="space-y-2">
{data.steps.map((st, sIdx) => (
<div
key={sIdx}
className="flex items-start gap-2.5 rounded border border-black/[0.06] dark:border-white/[0.06] bg-white dark:bg-[#222222] p-2.5"
>
<span className="flex size-5 shrink-0 items-center justify-center rounded bg-[#185ABD]/10 font-mono text-[11px] font-bold text-[#185ABD] dark:text-[#8BB8FF]">
{st.num}
</span>
<div className="flex-1 min-w-0">
<div className="text-[11.5px] font-bold text-[#1A1A1A] dark:text-white leading-tight">
{st.title}
</div>
<div className="text-[11px] text-[#1A1A1A]/80 dark:text-[#D0D0D0] mt-0.5 leading-snug">
{st.desc}
</div>
</div>
</div>
))}
</div>
</div>
</div>
</div>
{/* Pied de page Word */}
<div className="pt-2.5 border-t border-black/[0.08] dark:border-white/[0.08] flex items-center justify-between text-[11px]">
<div className="flex items-center gap-1.5 text-[#185ABD] dark:text-[#6BA5FF] font-semibold">
<ShieldCheck className="w-3.5 h-3.5" />
<span>{data.guaranteeText}</span>
</div>
<div className="text-[#1A1A1A]/70 dark:text-white/60 font-mono">
Wordly Engine Microsoft Word XML
</div>
</div>
</div>
);
}
/**
* Rendu intérieur d'une feuille Excel (.xlsx) avec vraie grille de cellules
*/
function ExcelDocumentSheet({ data }: { data: RecipeData }) {
return (
<div className="w-full h-full bg-white dark:bg-[#1E1E1E] text-[#1A1A1A] dark:text-[#EDEDED] font-sans p-3 sm:p-5 flex flex-col justify-between select-none">
{/* Barre de formule Excel */}
<div className="flex items-center gap-2 px-2 py-1.5 bg-[#F3F2F1] dark:bg-[#2D2D2D] rounded border border-black/10 dark:border-white/10 text-[11px] font-mono mb-3">
<span className="font-bold text-[#107C41] dark:text-[#42CA78] px-1.5 py-0.5 bg-white dark:bg-[#1E1E1E] rounded border border-black/10">
A1:D8
</span>
<span className="text-[#1A1A1A]/40 dark:text-white/40">fx</span>
<span className="text-[#107C41] dark:text-[#42CA78] truncate font-medium">
{data.excelFormula}
</span>
</div>
{/* Grille Excel avec entêtes de colonnes A, B, C, D */}
<div className="border border-[#107C41]/30 dark:border-white/15 rounded overflow-hidden shadow-xs my-auto">
{/* Ligne des colonnes A B C D */}
<div className="grid grid-cols-12 bg-[#F3F2F1] dark:bg-[#2D2D2D] border-b border-black/10 dark:border-white/10 text-[10.5px] font-mono font-bold text-center text-[#1A1A1A]/75 dark:text-white/70 divide-x divide-black/10 dark:divide-white/10">
<div className="col-span-1 py-1 bg-[#E1DFDD] dark:bg-[#383838]">#</div>
<div className="col-span-4 py-1">A ({data.excelHeaders[0]})</div>
<div className="col-span-2 py-1">B ({data.excelHeaders[1]})</div>
<div className="col-span-2 py-1">C ({data.excelHeaders[2]})</div>
<div className="col-span-3 py-1">D ({data.excelHeaders[3]})</div>
</div>
{/* Ligne 1 : Titre fusionné */}
<div className="grid grid-cols-12 bg-[#107C41]/10 dark:bg-[#107C41]/20 border-b border-black/10 dark:border-white/10 divide-x divide-black/10 dark:divide-white/10 items-center">
<div className="col-span-1 py-1.5 text-center font-mono text-[10.5px] font-bold bg-[#F3F2F1] dark:bg-[#2D2D2D] text-[#1A1A1A]/75 dark:text-white/70">
1
</div>
<div className="col-span-11 px-3 py-1.5 font-serif font-bold text-[13px] text-[#107C41] dark:text-[#52D886] truncate">
{data.docTitle}
</div>
</div>
{/* Lignes de données Excel */}
{data.excelRows.map((row, rIdx) => (
<div
key={rIdx}
className="grid grid-cols-12 border-b border-black/[0.06] dark:border-white/[0.06] divide-x divide-black/[0.06] dark:divide-white/[0.06] text-[11.5px] items-center hover:bg-black/[0.02]"
>
<div className="col-span-1 py-1 text-center font-mono text-[10.5px] font-semibold bg-[#F3F2F1] dark:bg-[#2D2D2D] text-[#1A1A1A]/70 dark:text-white/60">
{rIdx + 2}
</div>
<div className="col-span-4 px-2 py-1 font-medium text-[#1A1A1A] dark:text-white truncate">
{row[0]}
</div>
<div className="col-span-2 px-2 py-1 font-mono text-center font-bold text-[#107C41] dark:text-[#42CA78]">
{row[1]}
</div>
<div className="col-span-2 px-2 py-1 text-center text-[#8B6F47] dark:text-[#C5A17A] truncate">
{row[2]}
</div>
<div className="col-span-3 px-2 py-1 text-[#1A1A1A]/85 dark:text-white/80 truncate">
{row[3]}
</div>
</div>
))}
{/* Ligne de total / formule */}
<div className="grid grid-cols-12 bg-[#F3F2F1]/70 dark:bg-[#252525] divide-x divide-black/10 dark:divide-white/10 items-center font-bold text-[11px]">
<div className="col-span-1 py-1 text-center font-mono text-[10.5px] bg-[#E1DFDD] dark:bg-[#383838] text-[#1A1A1A]/75 dark:text-white/70">
7
</div>
<div className="col-span-6 px-3 py-1 text-[#107C41] dark:text-[#52D886] truncate">
{data.excelTotalLabel}
</div>
<div className="col-span-5 px-3 py-1 font-mono text-end text-[#1A1A1A] dark:text-white truncate">
{data.excelTotalVal}
</div>
</div>
</div>
{/* Onglets de classeur Excel en bas */}
<div className="flex items-center gap-1 text-[11px] font-medium pt-2 border-t border-black/10 dark:border-white/10">
<div className="px-3 py-1 bg-white dark:bg-[#2A2A2A] border-t-2 border-[#107C41] font-bold text-[#107C41] dark:text-[#42CA78] shadow-xs">
Feuil1_Recette
</div>
<div className="px-3 py-1 text-[#1A1A1A]/70 dark:text-white/60">
Feuil2_Calculs
</div>
<div className="ml-auto text-[11px] text-[#107C41] dark:text-[#42CA78] font-semibold flex items-center gap-1">
<ShieldCheck className="w-3.5 h-3.5" />
<span>Formules & Cellules 100% intactes</span>
</div>
</div>
</div>
);
}
/**
* Rendu intérieur d'une diapositive PowerPoint (.pptx)
*/
function PptxDocumentSheet({ data }: { data: RecipeData }) {
return (
<div className="w-full h-full bg-[#FAF8F5] dark:bg-[#1E1E1E] text-[#1A1A1A] dark:text-[#EDEDED] font-sans p-4 sm:p-6 flex flex-col justify-between select-none">
{/* Diapositive 16:9 avec cartouche PowerPoint */}
<div className="border-b border-[#C43E1C]/20 dark:border-white/10 pb-2.5 flex items-center justify-between">
<div className="flex items-center gap-2">
<span className="px-2 py-0.5 rounded bg-[#C43E1C]/10 text-[#C43E1C] dark:text-[#FF7F5E] text-[11px] font-bold font-mono">
Diapositive 1 / 1
</span>
<span className="text-[11.5px] font-semibold text-[#1A1A1A] dark:text-white">
{data.pptSlideTitle}
</span>
</div>
<span className="text-[11px] font-mono text-[#8B6F47] dark:text-[#C5A17A]">
Thème Officiel 16:9
</span>
</div>
{/* Cartes de contenu de présentation */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 my-auto py-2">
<div className="rounded-lg border-2 border-dashed border-[#C43E1C]/30 bg-white dark:bg-[#252525] p-3 sm:p-4 shadow-xs">
<div className="flex items-center gap-2 text-[#C43E1C] dark:text-[#FF7F5E] font-bold text-[12px] mb-1.5">
<span className="size-2 rounded-full bg-[#C43E1C]" />
{data.pptCard1Title}
</div>
<p className="text-[11.5px] text-[#1A1A1A]/85 dark:text-[#D5D5D5] leading-relaxed">
{data.pptCard1Text}
</p>
</div>
<div className="rounded-lg border-2 border-dashed border-[#C43E1C]/30 bg-white dark:bg-[#252525] p-3 sm:p-4 shadow-xs">
<div className="flex items-center gap-2 text-[#C43E1C] dark:text-[#FF7F5E] font-bold text-[12px] mb-1.5">
<span className="size-2 rounded-full bg-[#C43E1C]" />
{data.pptCard2Title}
</div>
<p className="text-[11.5px] text-[#1A1A1A]/85 dark:text-[#D5D5D5] leading-relaxed">
{data.pptCard2Text}
</p>
</div>
</div>
<div className="pt-2 border-t border-black/10 dark:border-white/10 flex items-center justify-between text-[11px]">
<span className="text-[#C43E1C] dark:text-[#FF7F5E] font-bold flex items-center gap-1">
<ShieldCheck className="w-3.5 h-3.5" />
Blocs textes & vecteurs PowerPoint préservés
</span>
<span className="text-[#1A1A1A]/60 dark:text-white/50 font-mono">
Affichage Normal 100%
</span>
</div>
</div>
);
}
/**
* Rendu intérieur d'un document PDF vectoriel officiel
*/
function PdfDocumentSheet({ data }: { data: RecipeData }) {
return (
<div className="w-full h-full bg-[#F5F5F3] dark:bg-[#191919] p-3 sm:p-5 flex flex-col justify-between select-none">
{/* Barre d'outils Acrobat */}
<div className="flex items-center justify-between px-3 py-1.5 bg-[#EBE9E4] dark:bg-[#252525] rounded text-[11px] font-mono border border-black/10 dark:border-white/10 mb-2">
<div className="flex items-center gap-2">
<span className="px-2 py-0.5 bg-white dark:bg-[#181818] rounded font-bold text-[#E03E2D]">
PDF/A-1b
</span>
<span className="text-[#1A1A1A]/80 dark:text-white/70">Page 1 / 1</span>
</div>
<div className="flex items-center gap-3 text-[#1A1A1A]/75 dark:text-white/60">
<span className="flex items-center gap-1"><ZoomIn className="w-3 h-3" /> 100%</span>
<span className="flex items-center gap-1"><Printer className="w-3 h-3" /> Vectoriel</span>
</div>
</div>
{/* Page PDF avec repères de coupe */}
<div className="flex-1 rounded border border-black/15 dark:border-white/15 bg-white dark:bg-[#202020] p-4 sm:p-6 shadow-sm flex flex-col justify-between">
<div>
<div className="flex items-center justify-between border-b border-black/10 dark:border-white/10 pb-2 mb-3">
<h1 className="font-serif text-[17px] sm:text-[20px] font-bold text-[#1A1A1A] dark:text-white">
{data.docTitle}
</h1>
<span className="font-mono text-[10px] text-[#E03E2D] px-2 py-0.5 rounded bg-[#E03E2D]/10">
DOCUMENT CERTIFIÉ
</span>
</div>
<p className="text-[11.5px] text-[#1A1A1A]/80 dark:text-white/80 leading-relaxed mb-3">
{data.docSubtitle} {data.timeLabel} {data.servingsLabel} {data.heatLabel}.
</p>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 text-[11px]">
<div className="p-2.5 rounded bg-[#FAF8F5] dark:bg-[#181818] border border-black/5 dark:border-white/5">
<span className="font-bold text-[#E03E2D] block mb-1">{data.ingredientsHeading}</span>
{data.ingredients.slice(0, 3).map((ing, i) => (
<div key={i} className="flex justify-between py-0.5">
<span className="text-[#1A1A1A] dark:text-[#E0E0E0]">{ing.name}</span>
<span className="font-mono font-bold text-[#8B6F47] dark:text-[#C5A17A]">{ing.amount}</span>
</div>
))}
</div>
<div className="p-2.5 rounded bg-[#FAF8F5] dark:bg-[#181818] border border-black/5 dark:border-white/5">
<span className="font-bold text-[#E03E2D] block mb-1">{data.stepsHeading}</span>
{data.steps.slice(0, 2).map((st, i) => (
<div key={i} className="py-0.5 leading-snug text-[#1A1A1A]/85 dark:text-[#D5D5D5]">
<strong className="text-[#1A1A1A] dark:text-white">{st.num}.</strong> {st.desc}
</div>
))}
</div>
</div>
</div>
<div className="pt-2 border-t border-black/10 dark:border-white/10 flex justify-between items-center text-[10.5px] text-[#1A1A1A]/70 dark:text-white/60 font-mono">
<span>Adobe PostScript Calque vectoriel préservé</span>
<span className="text-[#E03E2D] font-bold">100% Fidélité PDF</span>
</div>
</div>
</div>
);
}
/**
* Composant principal de démonstration
*/
export function HeroVisual() {
const prefersReduced = useReducedMotion();
const [format, setFormat] = useState<OfficeFormat>('docx');
const [langIndex, setLangIndex] = useState(0);
const [prevLangIndex, setPrevLangIndex] = useState(0);
const [isTranslating, setIsTranslating] = useState(false);
const [scanProgress, setScanProgress] = useState(0);
const cfg = FORMAT_CONFIG[format];
const currentRecipe = RECIPES[langIndex];
const prevRecipe = RECIPES[prevLangIndex];
// Déclencher une traduction manuelle ou automatique
const triggerTranslation = useCallback((targetLangIndex: number) => {
if (targetLangIndex === langIndex || isTranslating) return;
setPrevLangIndex(langIndex);
setLangIndex(targetLangIndex);
setIsTranslating(true);
setScanProgress(0);
const startTime = performance.now();
const animateScan = (currentTime: number) => {
const elapsed = currentTime - startTime;
const progress = Math.min(elapsed / SCAN_DURATION_MS, 1);
setScanProgress(progress * 100);
if (progress < 1) {
requestAnimationFrame(animateScan);
} else {
setIsTranslating(false);
setPrevLangIndex(targetLangIndex);
}
};
requestAnimationFrame(animateScan);
}, [langIndex, isTranslating]);
// Boucle de rotation automatique des langues (toutes les ~6.2 secondes)
useEffect(() => {
if (prefersReduced) return;
const interval = setInterval(() => {
setLangIndex((current) => {
const next = (current + 1) % RECIPES.length;
setPrevLangIndex(current);
setIsTranslating(true);
setScanProgress(0);
const startTime = performance.now();
const animateScan = (currentTime: number) => {
const elapsed = currentTime - startTime;
const progress = Math.min(elapsed / SCAN_DURATION_MS, 1);
setScanProgress(progress * 100);
if (progress < 1) {
requestAnimationFrame(animateScan);
} else {
setIsTranslating(false);
setPrevLangIndex(next);
}
};
requestAnimationFrame(animateScan);
return next;
});
}, CYCLE_DURATION_MS);
return () => clearInterval(interval);
}, [prefersReduced]);
// Sélecteur de rendu selon le format actif
const renderSheet = (data: RecipeData) => {
switch (format) {
case 'xlsx':
return <ExcelDocumentSheet data={data} />;
case 'pptx':
return <PptxDocumentSheet data={data} />;
case 'pdf':
return <PdfDocumentSheet data={data} />;
case 'docx':
default:
return <WordDocumentSheet data={data} />;
}
};
return (
<div className="w-full relative mx-auto select-none">
{/* Halo d'ambiance doré sous la fenêtre */}
<div className="absolute -inset-2 bg-gradient-to-r from-brand-accent/30 via-amber-400/20 to-brand-accent/30 rounded-3xl blur-2xl opacity-70 pointer-events-none -z-10" />
{/* Pastille Flottante 1 (Gauche) */}
<div className="hidden xl:flex floating-badge absolute -left-12 top-16 z-30 items-center gap-3 px-4 py-2.5 rounded-2xl bg-white/95 dark:bg-[#1f1f1f]/95 border border-brand-accent/40 shadow-2xl backdrop-blur-xl">
<div className="w-8 h-8 rounded-xl bg-emerald-500/15 text-emerald-600 dark:text-emerald-400 flex items-center justify-center">
<ShieldCheck className="w-4 h-4" />
</div>
<div className="text-left">
<div className="text-xs font-bold text-brand-dark dark:text-white">SmartArt & Formules</div>
<div className="text-[10px] text-brand-dark/60 dark:text-white/60 font-medium">100% Intacts et recalculés</div>
</div>
</div>
{/* Pastille Flottante 2 (Droite) */}
<div className="hidden xl:flex floating-badge-delayed absolute -right-12 top-32 z-30 items-center gap-3 px-4 py-2.5 rounded-2xl bg-white/95 dark:bg-[#1f1f1f]/95 border border-brand-accent/40 shadow-2xl backdrop-blur-xl">
<div className="w-8 h-8 rounded-xl bg-blue-500/15 text-blue-600 dark:text-blue-400 flex items-center justify-center">
<Sparkles className="w-4 h-4 text-[#185ABD] dark:text-[#6BA5FF]" />
</div>
<div className="text-left">
<div className="text-xs font-bold text-brand-dark dark:text-white">Rétention Zéro</div>
<div className="text-[10px] text-brand-dark/60 dark:text-white/60 font-medium">Données supprimées après export</div>
</div>
</div>
{/* Conteneur de fenêtre logicielle Office / PDF ultra-réaliste */}
<div className="relative rounded-2xl md:rounded-3xl border border-black/15 dark:border-white/15 bg-[#FDFCF9] dark:bg-[#141414] shadow-[0_25px_60px_-15px_rgba(0,0,0,0.18)] dark:shadow-[0_25px_60px_-15px_rgba(0,0,0,0.8)] overflow-hidden">
{/* ============================================================ */}
{/* 1. BARRE DE TITRE OFFICIELLE DU LOGICIEL (Word/Excel/PPT/PDF) */}
{/* ============================================================ */}
<div className={cn('px-3.5 py-2 sm:px-4 sm:py-2.5 flex items-center justify-between transition-colors duration-300', cfg.headerBg, cfg.headerText)}>
{/* Gauche : Icônes d'accès rapide (Enregistrer, Annuler, Rétablir) */}
<div className="flex items-center gap-2 sm:gap-3">
<div className="flex items-center gap-1.5 pr-2 border-r border-white/20">
<span className="w-2.5 h-2.5 rounded-full bg-white/25 hover:bg-white/40" />
<span className="w-2.5 h-2.5 rounded-full bg-white/25 hover:bg-white/40" />
<span className="w-2.5 h-2.5 rounded-full bg-white/25 hover:bg-white/40" />
</div>
<div className="hidden sm:flex items-center gap-2 text-white/80">
<Save className="w-3.5 h-3.5 hover:text-white cursor-pointer" />
<Undo className="w-3.5 h-3.5 hover:text-white cursor-pointer" />
<Redo className="w-3.5 h-3.5 hover:text-white cursor-pointer" />
</div>
<span className="font-mono text-[11px] sm:text-[12px] font-bold tracking-tight text-white flex items-center gap-1.5">
<cfg.icon className="w-3.5 h-3.5" />
<span>{cfg.defaultFile} {cfg.appTitle}</span>
</span>
</div>
{/* Centre : Recherche Office */}
<div className="hidden md:flex items-center gap-1.5 px-3 py-0.5 rounded bg-black/20 text-white/70 text-[11px] font-sans">
<Search className="w-3 h-3" />
<span>Rechercher ou demander de l&apos;aide (Alt+Q)</span>
</div>
{/* Droite : Boutons de fenêtre Windows / Mac */}
<div className="flex items-center gap-2 text-white/80">
<Minus className="w-3.5 h-3.5 cursor-pointer hover:text-white" />
<Square className="w-3 h-3 cursor-pointer hover:text-white" />
<X className="w-3.5 h-3.5 cursor-pointer hover:text-white" />
</div>
</div>
{/* ============================================================ */}
{/* 2. RUBAN OFFICE / SÉLECTEUR DE FORMAT RÉEL */}
{/* ============================================================ */}
<div className="bg-[#F3F2F1] dark:bg-[#202020] border-b border-black/10 dark:border-white/10 px-3 sm:px-5 py-2 flex flex-wrap items-center justify-between gap-2.5">
{/* Onglets des 4 formats bureautiques supportés */}
<div className="flex items-center gap-1 sm:gap-1.5">
{(['docx', 'xlsx', 'pptx', 'pdf'] as const).map((fmtKey) => {
const item = FORMAT_CONFIG[fmtKey];
const isSelected = format === fmtKey;
const Icon = item.icon;
return (
<button
key={fmtKey}
onClick={() => setFormat(fmtKey)}
className={cn(
'flex items-center gap-1.5 px-2.5 py-1 rounded text-[11.5px] font-bold font-mono transition-all duration-200 cursor-pointer border',
isSelected
? 'bg-white dark:bg-[#303030] text-[#1A1A1A] dark:text-white border-black/15 dark:border-white/20 shadow-xs ring-1 ring-black/5'
: 'border-transparent text-[#1A1A1A]/75 dark:text-white/70 hover:bg-black/5 dark:hover:bg-white/5'
)}
title={`Basculer vers le format ${item.label}`}
>
<Icon className="w-3.5 h-3.5" style={{ color: item.accentColor }} />
<span>{item.ext}</span>
</button>
);
})}
</div>
{/* Outils du ruban (Police, Gras, Italique, Alignement) */}
<div className="hidden lg:flex items-center gap-2 pl-3 border-l border-black/10 dark:border-white/10 text-[#1A1A1A]/80 dark:text-white/80">
<div className="flex items-center gap-1 px-2 py-0.5 rounded bg-white dark:bg-[#2A2A2A] border border-black/10 text-[11px] font-mono">
<span>{format === 'docx' ? 'Calibri' : format === 'xlsx' ? 'Segoe UI' : 'Arial'}</span>
<ChevronDown className="w-3 h-3" />
</div>
<div className="flex items-center gap-0.5">
<button className="p-1 rounded hover:bg-black/10 dark:hover:bg-white/10"><Bold className="w-3.5 h-3.5" /></button>
<button className="p-1 rounded hover:bg-black/10 dark:hover:bg-white/10"><Italic className="w-3.5 h-3.5" /></button>
<button className="p-1 rounded hover:bg-black/10 dark:hover:bg-white/10"><Underline className="w-3.5 h-3.5" /></button>
</div>
<div className="h-3 w-px bg-black/10 dark:bg-white/10" />
<div className="flex items-center gap-0.5">
<button className="p-1 rounded hover:bg-black/10 dark:hover:bg-white/10"><AlignLeft className="w-3.5 h-3.5" /></button>
<button className="p-1 rounded hover:bg-black/10 dark:hover:bg-white/10"><AlignCenter className="w-3.5 h-3.5" /></button>
<button className="p-1 rounded hover:bg-black/10 dark:hover:bg-white/10"><List className="w-3.5 h-3.5" /></button>
<button className="p-1 rounded hover:bg-black/10 dark:hover:bg-white/10"><TableIcon className="w-3.5 h-3.5" /></button>
</div>
</div>
{/* Sélecteur de langue en direct */}
<div className="flex items-center gap-1.5 ml-auto">
<span className="text-[11px] font-semibold text-[#8B6F47] dark:text-[#C5A17A] hidden sm:inline">
Langue :
</span>
<div className="flex items-center rounded-lg bg-black/[0.05] dark:bg-white/[0.08] p-0.5 border border-black/10 dark:border-white/10">
{RECIPES.map((r, rIdx) => {
const isActive = rIdx === langIndex;
return (
<button
key={r.lang}
onClick={() => triggerTranslation(rIdx)}
className={cn(
'px-2 py-0.5 rounded text-[11px] font-bold font-mono transition-all duration-200 cursor-pointer',
isActive
? 'bg-[#C5A17A] text-white shadow-xs'
: 'text-[#1A1A1A]/75 dark:text-white/70 hover:text-[#1A1A1A] dark:hover:text-white'
)}
>
{r.lang}
</button>
);
})}
</div>
</div>
</div>
{/* ============================================================ */}
{/* 3. RÈGLE GRADUÉE WORD (Visible en mode .docx) */}
{/* ============================================================ */}
{format === 'docx' && (
<div className="hidden sm:flex items-center justify-between px-8 py-0.5 bg-[#FAF8F5] dark:bg-[#1A1A1A] border-b border-black/[0.06] dark:border-white/[0.06] text-[9.5px] font-mono text-[#1A1A1A]/60 dark:text-white/60">
<span> 0</span>
<span>2</span>
<span>4</span>
<span>6</span>
<span>8</span>
<span>10</span>
<span>12</span>
<span>14</span>
<span>16 cm </span>
</div>
)}
{/* ============================================================ */}
{/* 4. PLAN DE TRAVAIL & DOCUMENT A4 FLOTTANT */}
{/* ============================================================ */}
<div className="p-3 sm:p-5 md:p-6 bg-[#EBE9E4] dark:bg-[#121212] flex items-center justify-center min-h-[380px]">
{/* Feuille de document flottante avec ombre de papier réelle */}
<div className="relative w-full max-w-4xl rounded-lg sm:rounded-xl overflow-hidden border border-black/10 dark:border-white/10 bg-white dark:bg-[#1C1C1C] shadow-[0_12px_35px_rgba(0,0,0,0.12)] dark:shadow-[0_12px_35px_rgba(0,0,0,0.6)]">
{/* Conteneur Superposé Grid garantissant 0px de décalage */}
<div className="relative grid grid-cols-1 grid-rows-1">
{/* Couche 1 : Document de base (Langue précédente ou active) */}
<div className="col-start-1 row-start-1 w-full h-full">
{renderSheet(isTranslating ? prevRecipe : currentRecipe)}
</div>
{/* Couche 2 : Document traduit révélé par découpe (Clip-path) */}
{isTranslating && !prefersReduced && (
<div
className="col-start-1 row-start-1 w-full h-full pointer-events-none"
style={{
clipPath: `inset(0% 0% ${100 - scanProgress}% 0%)`,
}}
>
{renderSheet(currentRecipe)}
</div>
)}
{/* ============================================================ */}
{/* 5. ONDE DORÉE DE NUMÉRISATION / TRADUCTION */}
{/* ============================================================ */}
{isTranslating && !prefersReduced && (
<div
className="pointer-events-none absolute inset-x-0 z-30 transition-transform duration-75"
style={{
top: `${scanProgress}%`,
transform: 'translateY(-50%)',
}}
>
{/* Halo lumineux doré au-dessus */}
<div className="h-16 w-full -mt-16 bg-gradient-to-b from-transparent via-[#C5A17A]/15 to-[#C5A17A]/30" />
{/* Ligne laser dorée principale */}
<div className="relative h-[2.5px] w-full bg-gradient-to-r from-transparent via-[#C5A17A] to-transparent shadow-[0_0_15px_#C5A17A,0_0_5px_#FFE4B5]" />
{/* Badge central d'indication de traduction */}
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 flex items-center gap-1.5 px-3 py-0.5 rounded-full bg-[#1A1A1A] text-[#C5A17A] border border-[#C5A17A]/70 shadow-[0_0_15px_rgba(197,161,122,0.5)] text-[11px] font-mono font-bold tracking-wider uppercase">
<Sparkles className="w-3 h-3 text-[#C5A17A] animate-spin" />
<span>Traduction en direct : {prevRecipe.lang} {currentRecipe.lang}</span>
</div>
</div>
)}
</div>
</div>
</div>
{/* ============================================================ */}
{/* 6. BARRE D'ÉTAT OFFICIELLE EN BAS (Statistiques & Langue) */}
{/* ============================================================ */}
<div className={cn('px-4 py-2 sm:px-6 sm:py-2.5 flex flex-wrap items-center justify-between gap-2 text-[11px] font-sans text-white transition-colors duration-300', cfg.statusBg)}>
{/* Statistiques documentaires officielles */}
<div className="flex items-center gap-3 sm:gap-4 font-medium">
<span>Page 1 sur 1</span>
<span className="hidden sm:inline text-white/40">|</span>
<span className="hidden sm:inline">184 mots</span>
<span className="text-white/40">|</span>
<span className="font-bold flex items-center gap-1 bg-black/20 px-2 py-0.5 rounded">
<span>{currentRecipe.langName}</span>
</span>
</div>
{/* Sceau de conformité Wordly.art */}
<div className="flex items-center gap-2 font-medium">
<Check className="w-3.5 h-3.5 text-emerald-300 stroke-[3]" />
<span className="hidden md:inline">Mise en page 100% inviolée :</span>
<span className="font-bold font-mono">0 pixel de décalage</span>
</div>
{/* Indicateur de Zoom */}
<div className="hidden sm:flex items-center gap-2 text-white/80 font-mono text-[10.5px]">
<span>Mode Page</span>
<span>100%</span>
</div>
</div>
</div>
</div>
);
}