All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 2m43s
1012 lines
45 KiB
TypeScript
1012 lines
45 KiB
TypeScript
'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'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>
|
||
);
|
||
}
|