---
suivi: 1641
date: 2026-09-22
sujet: Haut de page compact du scénario de faisabilité (identité, documents, ancres, assiette, fiche foncière)
chantier: Foncier / Faisabilité
type: refonte-affichage
statut: poussé
hash: f1548f6e
fichiers:
  - resources/js/Pages/Foncier/Scenarios/Show.vue
  - resources/js/Components/Foncier/ScenarioKpiBar.vue
  - tests/Unit/ScenarioHautDePageCompact1641SourceTest.php
  - tests/Unit/ScenarioKpiBar1634SourceTest.php
  - tests/Unit/FaisabiliteSaisieTableur1636SourceTest.php
  - database/data/journal_mises_a_jour_post_2026_06_12.php
---

# PROMPT ENVOYÉ

Premier des trois lots de refonte tableur de `/foncier/scenarios/{id}`. Ne traite QUE le haut
de page : identité repliée dans la ligne du titre, documents discrets derrière un bouton
compteur, barre d'ancres avec section active au défilement, assiette compactée (pastilles
redondantes supprimées, recherche à droite, colonne Commune, ligne TOTAL), fiche foncière
dégraissée (paragraphes dans un « ? », trois cartes de recap en une ligne de 34 px). Une seule
zone collante. Aucun calcul modifié. Grille (#1642) et étude Fi (#1643) intouchées.

# SYNTHÈSE

## Ce qui a été DÉPLACÉ

| Élément | Avant | Après |
|---|---|---|
| Fil d'Ariane | `<nav>` pleine largeur, `text-sm`, 3 segments | dans la ligne du titre, `text-xs`, **2 segments** (le scénario n'est plus répété sous son propre titre) |
| Titre | `<h1 text-2xl>` + sur-titre « Scénario de faisabilité » + sous-titre mode · statut | `<h1 text-[19px] font-bold>` seul, sur la ligne du titre |
| Libellé | carte Identité, grille 2 colonnes | ligne du titre, `InputText` **116 px**, hauteur 28 px |
| Statut | carte Identité | ligne du titre, `Select` **98 px** |
| Mode | carte Identité, `Select` + paragraphe d'avertissement sous le champ | ligne du titre : **pastille + cadenas + infobulle** quand verrouillé, `Select` 110 px quand encore modifiable |
| Commentaire | carte Identité, `Textarea rows=2` permanent sur 2 colonnes | **dépliant d'une ligne** derrière un bouton bulle (voir arbitrage) |
| Bandeau « Gelé » | sous-titre du `<header>` | pastille ambre sur la ligne du titre, infobulle avec le programme |
| Aide d'identité | paragraphes épars | `HelpHint` « ? » sur la ligne du titre |
| Bouton Enregistrer (identité) | pied de la carte Identité | ligne du titre, `saveMeta` inchangé |
| Documents du scénario | `EntityDocumentsPanel` **déplié en permanence** (271 px) | bouton compteur « N documents », panneau au clic |
| Recherche de parcelle | barre pleine largeur au-dessus de l'assiette | à droite de l'en-tête de section, **230 px**, résultats en liste flottante |
| 3 paragraphes de la fiche foncière | empilés au-dessus du tableau | `HelpHint` « ? » de l'en-tête de section |
| 3 cartes de recap de la fiche foncière | grille de 3 cartes sous le tableau | **une ligne de totaux de 34 px** en pied de section, avec « ? » portant le rappel sans TVA |
| Barre d'ancres | n'existait pas | 3ᵉ rangée de la zone collante du #1640 |

## Ce qui a été SUPPRIMÉ

- Le sur-titre « Scénario de faisabilité » et le sous-titre « mode · statut » : redondants avec
  les champs Mode et Statut désormais visibles en ligne.
- Le `<h2>Identité</h2>` et sa carte : le bloc n'existe plus.
- **Les pastilles de parcelles redondantes.** Elles listaient TOUS les candidats du dossier, y
  compris ceux déjà présents dans le tableau juste dessous — et `addCandidate` (Show.vue:110)
  refusait déjà les doublons, donc ces pastilles étaient **inertes au clic**. Restent proposées
  uniquement les parcelles du dossier **non encore retenues** (`candidatsNonRetenus`), sur une
  rangée qui **disparaît d'elle-même** quand tout le dossier est retenu — le cas exact de la
  capture de Robin. Choix assumé plutôt qu'une suppression sèche : supprimer la rangée entière
  aurait retiré le seul chemin rapide pour ajouter une parcelle du dossier non retenue.
- Les paragraphes « Ventilation du prix d'acquisition… », « Les montants sont réservés au droit
  prix… » et le rappel de saisie tableur au-dessus du tableau de la fiche foncière : **le texte
  est conservé à l'identique** dans le « ? ».
- Les 3 cartes de recap : **aucune valeur perdue** (6 ventilations, base du prix de revient,
  frais de notaire, prix au m², convention, rappel sans TVA — tous vérifiés par test).

## ARBITRAGE — le champ Commentaire

Retenu : **dépliant d'une ligne**, ouvert par un bouton `pi pi-comment` placé après le « ? ».
Un point bleu sur le bouton signale un commentaire renseigné, pour qu'il ne devienne pas
invisible. Ouvert, le champ occupe une ligne pleine largeur sous la ligne du titre
(`Textarea rows=1 auto-resize`).

Pourquoi PAS derrière le « ? » : `HelpHint` est un panneau d'**aide en lecture seule**
(`role="dialog"` ouvert au survol, fermé à la sortie de souris et par Échap). Y placer un champ
de saisie signifierait perdre la frappe dès que la souris quitte le panneau, et mélanger deux
natures — documentation et donnée. Un commentaire est une donnée : il garde son propre
déclencheur.

## UNE SEULE ZONE COLLANTE — hauteurs chiffrées

La barre d'ancres a rejoint `#scenario-kpi-bar-host` (le conteneur téléporté du #1640), en 3ᵉ
rangée du MÊME `div`. Il n'existe qu'un `<Teleport>` et qu'un hôte : vérifié par test
(`substr_count` == 1 sur les deux). Aucune zone collante n'est empilée.

Hauteurs calculées depuis le modèle de boîte des classes appliquées (méthode : somme des
paddings, bordures et hauteurs de ligne déclarées — pas de mesure navigateur, l'application
n'étant pas exécutable en local faute de base) :

| Zone collante | Décomposition | Total |
|---|---|---|
| Avant (#1640 seul) | `py-1.5` 12 + rangée 1 (15 + 4 + 2) 21 + `gap-1` 4 + rangée 2 (14 + 4 + 2) 20 + `border-b-2` 2 | **59 px** |
| Après (#1641) | 59 + `mt-1` 4 + onglet d'ancre (14 + 8 + 2) 24 − `-mb-[2px]` 2 | **85 px** |

Coût de la barre d'ancres : **+26 px** de zone collante, pour un gain de 854 px de contenu.

| Du haut de la zone de contenu à la 1ʳᵉ ligne de données de l'assiette | Décomposition | Total |
|---|---|---|
| **AVANT** | `main p-8` 32 + `wrapper sm:p-6` 24 + fil d'Ariane 20 + `space-y-6` 24 + `<header>` (16+32+24) 72 + `space-y-6` 24 + panneau Documents déplié 271 + `space-y-6` 24 + carte Identité 314 + `space-y-6` 24 + assiette jusqu'à la 1ʳᵉ ligne 178 | **1 007 px** |
| **APRÈS** | `main p-8` 32 + `wrapper sm:p-4` 16 + ligne du titre 28 + `space-y-3` 12 + assiette jusqu'à la 1ʳᵉ ligne 65 | **153 px** |

Gain : **854 px, soit un rapport de 6,6**. Avec la rangée de candidats non retenus affichée
(cas où le dossier contient des parcelles non reprises) : 178 px, soit toujours un rapport de 5,7.

Détail du poste « assiette jusqu'à la 1ʳᵉ ligne » : avant = bordure 1 + `p-4` 16 + `h2` 24 +
`mt-3` 12 + recherche 36 + `space-y-2` 8 + rangée de pastilles 36 + `mt-3` 12 + `thead` 33 = 178 ;
après = bordure 1 + en-tête (`py-1.5` 12 + champ `h-7` 28 + bordure 1) 41 + `thead` 23 = 65.

Contrôle de cohérence avec le constat de Robin : sur 1 080 px de haut, la nav applicative (50)
+ le fil d'Ariane AppShell (45) + la zone collante (85) laissent **900 px** de contenu visible.
Avant ce lot, la première ligne de l'assiette tombait à 1 007 px, donc **sous la ligne de
flottaison** — exactement le « il faut défiler pour atteindre l'assiette » constaté. Après, elle
est à 153 px, avec ~750 px de données visibles d'un coup.

## Densité appliquée (assiette)

- Lignes de tableau `h-7` = **28 px**, dans la fourchette 27-28 px demandée.
- `border-collapse` + `border border-[#E5E7EB]` sur chaque cellule : bordures 1 px jointives,
  plus aucune carte arrondie entre les lignes.
- La saisie de surface remplit sa cellule sans bordure propre
  (`!border-0 !bg-transparent !px-2 !py-0`, cellule en `p-0`) : **la bordure de cellule EST la
  grille**.
- Nombres en `tabular-nums` alignés à droite ; ligne TOTAL sur fond `bg-[#F3F4F6]`.
- Cellules calculées de la fiche foncière : fond gris et non-éditabilité **inchangés**.

## Barre d'ancres — section active

`IntersectionObserver` sur les quatre sections (`#scenario-section-{assiette,fiche,grille,etude}`),
`root` = `document.querySelector('main')` (la vraie boîte de défilement, cf. #1640),
`rootMargin: '0px 0px -75% 0px'` pour que la section active soit celle qui occupe le HAUT de
l'écran et non celle qui affleure en bas. **Aucun listener de scroll**, donc rien à throttler
(vérifié par test : ni `addEventListener('scroll'`, ni `@scroll`). Observateur libéré dans
`onBeforeUnmount`.

Le clic appelle `scrollIntoView({ behavior: 'smooth', block: 'start' })` sans compensation
d'offset : la zone collante étant HORS de `main`, il n'y a aucun recouvrement à corriger.
`scroll-mt-2` est posé en ceinture sur les quatre sections.

Collision évitée : `sectionActive` existait déjà (#1634) et désigne la section **en cours
d'édition** pour le bouton flottant. L'état des ancres est donc `sectionVisible` — deux notions
distinctes, test dédié. Le build avait d'ailleurs échoué sur
`Identifier 'sectionActive' has already been declared`.

## Garde-fous respectés

Aucun fichier PHP applicatif modifié : ni `FaisabiliteLotissementMoteur`, ni
`ScenarioFicheFonciereService`, ni `ScenarioGrillePrixService`, ni `ScenarioEtudeFiService`, ni
le contrôleur. Aucune migration, aucune route. Les sections GRILLE DE PRIX et ÉTUDE FINANCIÈRE
ne reçoivent QUE leur attribut `id` et `scroll-mt-2` — vérifié par test sur la chaîne exacte de
leurs classes, inchangées par ailleurs. Les cinq boutons « Enregistrer » et le bouton flottant
du #1634/#1640 fonctionnent (test sur les cinq handlers). Montants toujours masqués sans
`peut_prix`. Scénario gelé : les trois champs remontés restent `:disabled="!peutEditer"`.
Pastilles d'unité et formules du #1640 en place. Pipeline documentaire, droits et limites
(50 Mo / 20 fichiers) inchangés : mêmes six props passées au même composant, seul le moment
d'affichage change. Aucun `overflow-x-hidden`.

Seule arithmétique ajoutée : `assietteTotalSurface`, somme d'affichage des surfaces retenues
(surface saisie, ou surface totale de la parcelle quand la saisie est vide — convention déjà
portée par le placeholder « Totale » du champ). Aucune règle métier nouvelle, aucun envoi au
serveur : `saveAssiette` continue de ne poster que `parcelle_id`, `surface_retenue_m2` et
`hypothetique`.

# DÉPLOIEMENT-TEST

Sur OVH : `git pull origin main` puis `php artisan optimize:clear`, `view:clear`,
`journal:sync`. Ni migration, ni `route:clear`.

1. Ouvrir un scénario en 1366 px puis 1920 px : aucun ascenseur horizontal, rien de tronqué,
   rien qui se chevauche. La première ligne de l'assiette est visible SANS défiler.
2. Ligne du titre : fil d'Ariane court, titre, Libellé, Statut, Mode, « ? », bouton Commentaire,
   puis Enregistrer / N documents / Retour dossier / Supprimer — tout sur une ligne.
3. Modifier le libellé depuis la ligne du titre, enregistrer, recharger : valeur conservée.
4. Cliquer le bouton bulle : le commentaire se déplie sur une ligne ; le saisir, enregistrer,
   recharger : conservé, et le point bleu apparaît sur le bouton.
5. Mode verrouillé : pastille grise avec cadenas et infobulle « Mode verrouillé : une saisie de
   calcul (fiche foncière) existe déjà. ». Sur un scénario sans saisie de calcul, le Mode reste
   un select modifiable.
6. Cliquer « N documents » : le panneau s'ouvre ; déposer un fichier puis le supprimer —
   comportement, droits et limites identiques à avant.
7. Barre d'ancres : cliquer ASSIETTE / FICHE FONCIÈRE / GRILLE DE PRIX / ÉTUDE FI → la page
   défile à la section. Défiler à la main → l'onglet actif suit, en onglet plein.
8. Assiette : l'en-tête affiche « N parcelles · X m² retenus », la recherche est à droite sur
   230 px, le tableau a une colonne Commune et une ligne TOTAL cohérente avec les surfaces.
9. Assiette : si toutes les parcelles du dossier sont retenues, AUCUNE rangée de pastilles ;
   sinon, seules les non retenues sont proposées et le clic les ajoute.
10. Fiche foncière : le « ? » de l'en-tête contient les trois explications ; le pied de section
    est une seule ligne de 34 px avec ventilations, base du prix de revient, frais de notaire et
    prix au m² ; son « ? » porte le rappel sur les propriétaires sans TVA.
11. Scénario GELÉ : libellé, statut et commentaire en lecture seule, Mode en pastille.
12. Compte sans `foncier.prix` : ventilations prix et base masquées, surfaces visibles.
13. Grille de prix et étude financière : strictement identiques à avant, saisie tableur
    (#1635/#1636) et pastilles d'unité (#1640) inchangées.
14. Les cinq boutons Enregistrer et le bouton flottant enregistrent bien leur propre section.

# LEÇON

- Sur cet écran, le haut de page consommait **1 007 px** avant la première donnée, dont **271 px**
  pour le seul panneau Documents déplié en permanence et **314 px** pour la carte Identité. Un
  panneau de dépôt de fichiers ouvert par défaut est le plus gros poste de hauteur perdue d'un
  écran de saisie : le replier derrière un compteur rend 271 px sans rien retirer.
- `space-y-6` + `p-4` par carte coûte ~50 px par section. Passer à `space-y-3` / en-têtes
  `py-1.5` sans carte rend l'équivalent d'un tiers d'écran sur une page à cinq sections.
- Vérifier les noms déjà pris avant d'ajouter un état : `sectionActive` existait depuis #1634
  avec une sémantique DIFFÉRENTE (section en édition vs section visible). Le compilateur SFC
  l'a signalé, mais un nom mal choisi qui aurait compilé aurait produit un bug silencieux.
- Un test source qui fige une écriture d'attributs (`label="X" size="small" :loading="Y"`) casse
  au premier reformatage sans qu'aucun comportement ne change. Préférer assertion sur le
  handler (`@click="saveMeta"`) et sur l'intention.
- Mon filtre de non-régression du #1640 (`--filter=Faisabilite`) ne couvrait PAS
  `ScenarioKpiBar1634SourceTest` : deux assertions de ce test étaient déjà cassées par #1640 et
  ne l'ont été constatées qu'ici. Sur cet écran, filtrer sur `Faisabilite|Scenario`.
- Une pastille de raccourci qui liste des éléments déjà présents dans le tableau voisin n'est
  pas seulement redondante : ici elle était **inerte** (le garde-fou anti-doublon de
  `addCandidate` la neutralisait). Filtrer la liste source vaut mieux que masquer le symptôme.
