---
suivi: 1436
date: 2026-09-04
sujet: Dashboard — en-tête discret + orientation liens rapides
chantier: dashboard
type: fix
statut: poussé
hash: 1d7fb857
fichiers:
  - resources/js/Layouts/AppShell.vue
  - resources/js/Pages/Dashboard/Index.vue
  - resources/js/Components/Dashboard/LiensRapidesWidget.vue
  - resources/js/Components/Dashboard/WidgetBody.vue
  - resources/js/Components/Dashboard/WidgetWrapper.vue
  - resources/js/Components/Dashboard/DashboardGrid.vue
  - app/Services/Dashboard/QuickLinksConfigValidator.php
  - app/Services/Dashboard/WidgetDataService.php
  - tests/Unit/QuickLinksConfigValidatorTest.php
  - tests/Unit/Dashboard1436SourceTest.php
  - database/data/journal_mises_a_jour_post_2026_06_12.php
  - public/build/
---

# SUIVI #1436 — En-tête accueil + disposition Liens rapides

## PROMPT ENVOYÉ

Ajustements visuels accueil après #1425/#1426 : sous-titre retiré ; mode édition et
ajout de widget dans la bande blanche, plus discrets ; widget Liens rapides sans
compteur 1/12, largeur au texte, orientation H/V dans `config` (pas de migration).
Ne pas défaire #1432 / #1433 / #1396. Pas de prune.

## FICHES LUES

- `SUIVI_1432` LEÇON — href seulement si URL allowlistée ; validate sur les deux
  écritures ; sanitize lecture. Conservé.
- `SUIVI_1433` — `localLayout.value = [...]` nouvelle référence. Conservé.
- `SUIVI_1396` LEÇON — pas de plancher hauteur en lecture. Conservé.
- `SUIVI_1426` — schéma `{ links: [...] }` ; MAX 12. Étendu, pas remplacé.
- `SUIVI_1271` — pas de masquer le débordement X.
- `SUIVI_1318` / `SUIVI_1270` — Teleport vers hôte AppShell page-spécifique.

## SYNTHÈSE

### Prérequis
`origin/main` contient #1435 (`317a10c3`) et #1438. Pas de collision Avis.

### Mesure bande blanche
`AppShell.vue` `<header class="flex items-center bg-white px-8 py-3">` (sous la
nav bleue). Slot `breadcrumb` : titres **hardcodés** par `$page.component`. Aucune
page n’y injecte d’actions via le slot. Les actions hors main passent par des
hôtes Teleport page-spécifiques (`#marche-subnav-host`, `#chantier-subnav-host`,
`#depot-saisie-subnav-host`).

Mécanisme réutilisé : hôte `#dashboard-header-host` **uniquement** si
`$page.component === 'Dashboard/Index'`, `v-else` inchangé pour les autres
écrans. Teleport `defer` depuis `Dashboard/Index.vue` (titre + actions).
AppShell modifié de façon bornée (pas de changement du `main.p-8` ni des autres
fils d’Ariane).

### Schéma `config`
`{ "links": [ … ], "orientation": "vertical" | "horizontal" }`
Défaut si clé absente : `"vertical"`. Configs #1426 sans clé restent valides.

### Validation orientation (3 niveaux)
1. Écriture `validate()` — `saveLayout` et `saveWidgetConfig` : absente → défaut ;
   autre que les deux valeurs → 422 `config.orientation`.
2. Lecture `sanitizeForRead()` — valeur invalide coercée en `vertical` **sans**
   jeter les liens.
3. Rendu — `orientation` seulement `horizontal` ou `vertical` ; libellé
   `{{ link.label }}` (échappé) ; `href` via `linkAttrs` (#1432).

## DÉPLOIEMENT-TEST

```
git pull origin main
php artisan optimize:clear
php artisan view:clear
php artisan journal:sync
```

Pas de migrate. Ne pas lancer prune.

## LEÇON
La bande blanche n’est pas un slot d’actions générique : y poser des boutons
d’une page = hôte `v-if` + Teleport, comme marché/chantier, pas un flex global
sur le header (ça déplacerait tous les écrans).
