feat(apercu): rend ajustable le partage entre PDF et données

Le volet PDF était figé à 40 % de la fenêtre. C'est le vrai levier de
lisibilité : élargir le volet augmente d'autant l'échelle du document, et
vers 768 px l'ajustement à la largeur vaut 97 %, soit 100 % de fait. La
poignée se glisse, la largeur est mémorisée et partagée entre l'écran
d'extraction et celui d'édition, le double-clic revient au défaut — porté
de 40 % à 50 % pour que la page tienne presque sans défiler.

Un voile couvre la fenêtre pendant le glissement : sans lui l'<iframe> du
PDF capte les évènements souris et le redimensionnement se fige dès qu'on
la survole.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-22 21:09:43 +02:00
parent e2e2f5bc0e
commit 552fc453dd
4 changed files with 221 additions and 12 deletions

View File

@@ -0,0 +1,117 @@
<template>
<div ref="conteneur" class="flex-1 flex overflow-hidden">
<div
class="flex flex-col overflow-hidden"
:class="classeGauche"
:style="{ width: `${largeurGauche}%` }"
>
<slot name="gauche" />
</div>
<!-- Bande de saisie de 8 px pour un trait de 1 px : viser un filet d'un
pixel à la souris est pénible. Les marges négatives annulent sa
largeur dans le calcul du flux, pour que les deux volets se partagent
bien 100 % ; la bande déborde donc de 4 px sur chacun, d' le z-10. -->
<div
class="group flex-shrink-0 w-2 -mx-1 z-10 flex justify-center cursor-col-resize"
role="separator"
aria-orientation="vertical"
title="Glisser pour redimensionner — double-clic pour réinitialiser"
@mousedown.prevent="demarrerGlissement"
@dblclick="reinitialiser"
>
<div
class="h-full w-px bg-gray-700 group-hover:bg-blue-500 transition-colors"
:class="{ '!bg-blue-500': glissement }"
/>
</div>
<div
class="flex flex-col overflow-hidden"
:class="classeDroite"
:style="{ width: `${100 - largeurGauche}%` }"
>
<slot name="droite" />
</div>
<!-- Voile actif seulement pendant le glissement. Sans lui, l'<iframe> du
PDF avale les évènements souris dès que le curseur la survole et le
redimensionnement se fige en cours de route. -->
<div v-if="glissement" class="fixed inset-0 z-50 cursor-col-resize" />
</div>
</template>
<script setup>
import { ref, onUnmounted } from 'vue'
const props = defineProps({
/** Clé localStorage : la largeur choisie est propre à chaque écran. */
cleStockage: { type: String, required: true },
/** Largeur du volet gauche en %, au premier affichage et au double-clic. */
largeurDefaut: { type: Number, default: 40 },
largeurMin: { type: Number, default: 20 },
largeurMax: { type: Number, default: 80 },
/** Classes posées sur les conteneurs de volet (fond, bordure...). */
classeGauche: { type: String, default: '' },
classeDroite: { type: String, default: '' }
})
const conteneur = ref(null)
const glissement = ref(false)
/** Le stockage peut être indisponible (navigation privée, réglages) ou
contenir une valeur hors bornes si les props ont changé depuis. */
function largeurMemorisee() {
try {
const valeur = Number(localStorage.getItem(props.cleStockage))
if (Number.isFinite(valeur) && valeur >= props.largeurMin && valeur <= props.largeurMax) {
return valeur
}
} catch {
/* stockage indisponible */
}
return props.largeurDefaut
}
const largeurGauche = ref(largeurMemorisee())
function memoriser(largeur) {
try {
localStorage.setItem(props.cleStockage, String(Math.round(largeur)))
} catch {
/* stockage indisponible */
}
}
function surDeplacement(evenement) {
const rect = conteneur.value?.getBoundingClientRect()
if (!rect?.width) return
const pourcentage = ((evenement.clientX - rect.left) / rect.width) * 100
largeurGauche.value = Math.min(props.largeurMax, Math.max(props.largeurMin, pourcentage))
}
function arreterGlissement() {
if (!glissement.value) return
glissement.value = false
document.removeEventListener('mousemove', surDeplacement)
document.removeEventListener('mouseup', arreterGlissement)
document.body.style.userSelect = ''
memoriser(largeurGauche.value)
}
function demarrerGlissement() {
glissement.value = true
document.addEventListener('mousemove', surDeplacement)
document.addEventListener('mouseup', arreterGlissement)
// Sans quoi le glissement sélectionne le texte des deux volets.
document.body.style.userSelect = 'none'
}
function reinitialiser() {
largeurGauche.value = props.largeurDefaut
memoriser(props.largeurDefaut)
}
// Un démontage en plein glissement laisserait les écouteurs sur `document`.
onUnmounted(arreterGlissement)
</script>

View File

@@ -68,9 +68,14 @@
</div>
<!-- Corps : Split view PDF + Données -->
<div v-else class="flex-1 flex overflow-hidden">
<VoletsAjustables
v-else
cle-stockage="apercuPdf.largeurVolet"
:largeur-defaut="50"
classe-droite="bg-gray-950"
>
<!-- Gauche : PDF Preview -->
<div class="w-2/5 border-r border-gray-700 flex flex-col">
<template #gauche>
<div class="flex-shrink-0 px-4 py-2 bg-gray-900 border-b border-gray-700">
<span class="card-title">Aperçu PDF</span>
</div>
@@ -91,10 +96,10 @@
</div>
</div>
</div>
</div>
</template>
<!-- Droite : Édition ou Tagging -->
<div class="w-3/5 flex flex-col bg-gray-950">
<template #droite>
<!-- Bandeau erreur de re-extraction -->
<div v-if="reExtractError" class="flex-shrink-0 px-4 py-2 bg-red-500/10 border-b border-red-500/30 text-sm text-red-400">
{{ reExtractError }}
@@ -138,8 +143,8 @@
<span>{{ saveError }}</span>
</div>
</div>
</div>
</div>
</template>
</VoletsAjustables>
</div>
</template>
@@ -150,6 +155,7 @@ import PdfPreview from '../components/PdfPreview.vue'
import JsonViewer from '../components/JsonViewer.vue'
import TaggingStep from '../components/TaggingStep.vue'
import ReExtractionDiff from '../components/ReExtractionDiff.vue'
import VoletsAjustables from '../components/VoletsAjustables.vue'
import { computeExtractionDiff } from '../utils/diffExtraction.js'
import { cheminRetour } from '../utils/retour.js'

View File

@@ -1,7 +1,11 @@
<template>
<div class="flex-1 flex overflow-hidden">
<VoletsAjustables
cle-stockage="apercuPdf.largeurVolet"
:largeur-defaut="50"
classe-droite="bg-gray-950"
>
<!-- Left panel: Upload or PDF Preview -->
<div class="w-2/5 flex flex-col border-r border-gray-700">
<template #gauche>
<!-- Upload zone when no file -->
<div v-if="!pdfFile" class="flex-1 flex items-center justify-center p-8 bg-gray-950">
<div class="max-w-md w-full">
@@ -36,10 +40,10 @@
:file-name="pdfFile.name"
class="flex-1"
/>
</div>
</template>
<!-- Right panel: JSON Viewer or Tagging Step -->
<div class="w-3/5 flex flex-col bg-gray-950">
<template #droite>
<!-- Extract button bar -->
<div v-if="pdfFile && !extractedData && !isExtracting && !showTagging" class="flex-shrink-0 p-4 bg-gray-900 border-b border-gray-700">
<button
@@ -111,8 +115,8 @@
:is-loading="isExtracting"
class="flex-1 overflow-hidden"
/>
</div>
</div>
</template>
</VoletsAjustables>
</template>
<script setup>
@@ -122,6 +126,7 @@ import { pendingFile } from '../store'
import PdfPreview from '../components/PdfPreview.vue'
import JsonViewer from '../components/JsonViewer.vue'
import TaggingStep from '../components/TaggingStep.vue'
import VoletsAjustables from '../components/VoletsAjustables.vue'
const router = useRouter()