Files
nav-carte/components/VueFiches.vue
T
Jules NenyandClaude Opus 5.5 d4e72942fc fix(vue-fiches): accessibilité et contrôles M8 (Lighthouse 24/24 ≥ 95)
Nom accessible de la carte-fiche = son contenu ; résumé et « Sans localisation »
en color-mix(--text 78 %) ; libellé du tri gardé pour les lecteurs d'écran sous
768 px ; lien « sans adresse » hors du parcours clavier quand le compteur est
masqué ; bouton Chatbot flottant en fond plein ; option sélectionnée explicite
des <select> (rendu serveur) ; pastille « Sans adresse » ; cluster allumé au
survol ; pas d'init Leaflet sur un conteneur détaché.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SCt6e8wWiYqRXLJAuKqP5i
2026-10-02 18:10:35 +02:00

815 lines
25 KiB
Vue
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.
<!--
VueFiches — B11-M3/M4. La vue fiches partagée (SPEC-vue-fiches) : barre
d'outils, trois états Carte / Mixte / Fiches, filtres à un seul endroit,
grille, poignée de repli. Elle ne charge pas les données et ne connaît pas la
carte : la page crée l'état (`useVueFiches`) et fournit le slot #carte.
États (classe racine) : carte · mixte · fiches · auto. `auto` = avant le
montage, quand l'URL ne tranche pas : la CSS rend Mixte à partir de 1024 px
et Fiches en dessous, pour qu'un lien s'affiche sans saut à l'hydratation.
Contrat complet : DESIGN-SYSTEM.md §Vue fiches.
-->
<template>
<div class="vf" :class="`vf--${vf.vueAffichee}`">
<!-- ═══════════ Barre d'outils -->
<div class="vf-barre">
<div class="vf-seg" role="group" aria-label="Affichage">
<button
v-for="e in etatsBoutons"
:key="e"
type="button"
class="vf-seg__btn"
:class="`vf-seg__btn--${e}`"
:aria-pressed="vf.vueAffichee === e"
@click="vf.setVue(e)"
>{{ LIBELLES_VUE[e] }}<span v-if="e === 'fiches'" class="vf-seg__n"> · {{ vf.resultat.length }}</span></button>
</div>
<label class="vf-recherche">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<circle cx="11" cy="11" r="8" /><line x1="21" y1="21" x2="16.65" y2="16.65" />
</svg>
<span class="vf-sr">{{ libelleRecherche }}</span>
<input
type="search"
:value="vf.q"
:placeholder="libelleRecherche"
autocomplete="off"
@input="vf.setQ(($event.target as HTMLInputElement).value)"
/>
<button v-if="vf.q" type="button" class="vf-recherche__effacer" aria-label="Effacer la recherche" @click="vf.setQ('')">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" aria-hidden="true"><line x1="18" y1="6" x2="6" y2="18" /><line x1="6" y1="6" x2="18" y2="18" /></svg>
</button>
</label>
<button
ref="boutonFiltrer"
type="button"
class="vf-filtrer"
aria-haspopup="dialog"
:aria-expanded="feuilleOuverte"
@click="ouvrirFeuille"
>Filtrer<template v-if="vf.nbActifs"> · {{ vf.nbActifs }}</template></button>
<!-- État Carte : seules les pastilles actives restent visibles -->
<div v-if="pastilles.length" class="vf-pastilles vf-pastilles--barre">
<Chip
v-for="p in pastilles"
:key="p.cle"
as="span"
removable
active
:variant="p.variante"
:label="p.libelle"
@remove="p.retirer()"
/>
</div>
<span class="vf-espace" aria-hidden="true" />
<p class="vf-compteur" aria-live="polite">
{{ vf.resultat.length }} {{ vf.resultat.length > 1 ? 'fiches' : 'fiche' }} sur {{ vf.total }}
<template v-if="vf.nbSansAdresse && !vf.etat.sansAdresse">
<span class="vf-compteur__sep" aria-hidden="true">·</span>
<button type="button" class="vf-lien" @click="vf.voirSansAdresse()">{{ vf.nbSansAdresse }} sans adresse</button>
</template>
</p>
<label class="vf-select vf-select--tri">
<span>Tri</span>
<select :value="vf.tri" @change="vf.setTri(($event.target as HTMLSelectElement).value as TriFiches)">
<option v-for="t in vf.cfg.tris" :key="t" :value="t" :selected="t === vf.tri">{{ LIBELLES_TRI[t] }}</option>
</select>
</label>
<label v-if="vf.cfg.groupes.length" class="vf-select vf-select--groupe">
<span>Regrouper</span>
<select :value="vf.etat.groupe ?? ''" @change="choisirGroupe">
<option value="" :selected="!vf.etat.groupe">aucun</option>
<option v-for="g in vf.cfg.groupes" :key="g.cle" :value="g.cle" :selected="g.cle === vf.etat.groupe">{{ g.libelle.toLowerCase() }}</option>
</select>
</label>
<label v-if="vf.cfg.sousVues" class="vf-select vf-select--carte">
<span class="vf-sr">Carte affichée</span>
<select :value="vf.etat.sousVue" @change="vf.setSousVue(($event.target as HTMLSelectElement).value)">
<option v-for="s in vf.cfg.sousVues.valeurs" :key="s.valeur" :value="s.valeur" :selected="s.valeur === vf.etat.sousVue">{{ s.libelle }}</option>
</select>
</label>
</div>
<!-- Téléphone, état Fiches : les pastilles actives sous la barre (les chips sont dans la feuille) -->
<div v-if="pastilles.length" class="vf-pastilles vf-pastilles--tel">
<Chip
v-for="p in pastilles"
:key="p.cle"
as="span"
removable
active
:variant="p.variante"
:label="p.libelle"
@remove="p.retirer()"
/>
</div>
<!-- ═══════════ Corps : liste | carte -->
<div class="vf-corps">
<section
class="vf-liste"
:aria-label="`Liste des fiches`"
:aria-hidden="vf.vueAffichee === 'carte' ? 'true' : undefined"
>
<p v-if="intention" class="vf-intention">{{ intention }}</p>
<VueFichesFiltres :vf="vf" id-base="vf-panneau" class="vf-filtres" />
<p v-if="pending" class="vf-etat-vide">Chargement des fiches…</p>
<div v-else-if="!vf.resultat.length" class="vf-etat-vide">
<slot name="vide">
<p>Aucune fiche ne correspond<template v-if="vf.q"> à « {{ vf.q }} »</template>.</p>
<button v-if="vf.nbActifs || vf.q" type="button" class="vf-lien" @click="vf.effacerTout()">Tout effacer</button>
</slot>
</div>
<template v-else>
<section
v-for="g in vf.groupes"
:key="g.cle"
class="vf-groupe"
:aria-labelledby="g.libelle ? `vf-g-${idGroupe(g.cle)}` : undefined"
>
<h3 v-if="g.libelle" :id="`vf-g-${idGroupe(g.cle)}`" class="vf-groupe__titre">
<button
type="button"
class="vf-groupe__bascule"
:aria-expanded="!replies.has(g.cle)"
@click="basculerGroupe(g.cle)"
>
<span aria-hidden="true" class="vf-groupe__chevron">{{ replies.has(g.cle) ? '›' : '⌄' }}</span>
{{ g.libelle }} · {{ g.fiches.length }}
</button>
</h3>
<div v-show="!replies.has(g.cle)" class="vf-grille">
<FicheMiniCard
v-for="f in g.fiches"
:key="`${f.kind}-${f.id}`"
:fiche="f"
etiquettes-cliquables
:etiquettes-actives="libellesActifs"
:actif="String(f.id) === String(vf.survolId ?? '') || String(f.id) === String(selectedId ?? '')"
@open="emit('select', f)"
@etiquette="(l) => ajouterDepuisCarte(f, l)"
@mouseenter="survoler(f)"
@mouseleave="survoler(null)"
/>
</div>
</section>
</template>
</section>
<div class="vf-carte">
<button
type="button"
class="vf-poignee"
:aria-label="vf.vueAffichee === 'carte' ? 'Afficher la liste des fiches' : 'Replier la liste des fiches'"
:aria-expanded="vf.vueAffichee !== 'carte'"
@click="vf.setVue(vf.vueAffichee === 'carte' ? 'mixte' : 'carte')"
><span aria-hidden="true">{{ vf.vueAffichee === 'carte' ? '›' : '‹' }}</span></button>
<slot name="carte" />
<!-- Téléphone, état Carte : pied « filtres actifs ✕ » + « Voir les N fiches › » -->
<div class="vf-pied">
<button v-if="vf.nbActifs || vf.q" type="button" class="vf-pied__filtres" @click="vf.effacerTout()">
<span class="vf-pied__resume">{{ resumeFiltres }}</span>
<span aria-hidden="true"> ✕</span><span class="vf-sr"> (tout effacer)</span>
</button>
<button type="button" class="vf-pied__voir" @click="vf.setVue('fiches')">
Voir {{ vf.resultat.length > 1 ? `les ${vf.resultat.length} fiches` : `${vf.resultat.length} fiche` }} ›
</button>
</div>
</div>
</div>
<!-- ═══════════ Feuille « Filtrer » (téléphone) -->
<Teleport to="body">
<div v-if="feuilleOuverte" class="vf-voile" @click="fermerFeuille" />
<div
v-if="feuilleOuverte"
ref="feuille"
class="vf-feuille"
role="dialog"
aria-modal="true"
aria-labelledby="vf-feuille-titre"
@keydown.esc="fermerFeuille"
@keydown.tab="pieger"
>
<div class="vf-feuille__tete">
<h2 id="vf-feuille-titre">Filtrer</h2>
<button ref="boutonFermer" type="button" class="vf-feuille__fermer" aria-label="Fermer les filtres" @click="fermerFeuille">✕</button>
</div>
<div class="vf-feuille__corps">
<VueFichesFiltres :vf="vf" id-base="vf-feuille" />
<label v-if="vf.cfg.groupes.length" class="vf-select vf-select--feuille">
<span>Regrouper</span>
<select :value="vf.etat.groupe ?? ''" @change="choisirGroupe">
<option value="" :selected="!vf.etat.groupe">aucun</option>
<option v-for="g in vf.cfg.groupes" :key="g.cle" :value="g.cle" :selected="g.cle === vf.etat.groupe">{{ g.libelle.toLowerCase() }}</option>
</select>
</label>
</div>
<div class="vf-feuille__pied">
<button type="button" class="vf-feuille__voir" @click="fermerFeuille">
Voir {{ vf.resultat.length > 1 ? `${vf.resultat.length} fiches` : `${vf.resultat.length} fiche` }}
</button>
</div>
</div>
</Teleport>
</div>
</template>
<script setup lang="ts">
import type { MiniFiche } from '~/composables/useFicheAdapter'
import type { VueFichesEtat } from '~/composables/useVueFiches'
import { LIBELLES_TRI, type EtatVue, type TriFiches } from '~/utils/vueFiches'
const props = defineProps<{
vf: VueFichesEtat
/** Une ligne, « intention d'abord » (DESIGN-SYSTEM §Intention d'abord). */
intention?: string
libelleRecherche: string
pending?: boolean
selectedId?: string | number | null
}>()
const emit = defineEmits<{
select: [fiche: MiniFiche]
survol: [fiche: MiniFiche | null]
}>()
const LIBELLES_VUE: Record<EtatVue, string> = { carte: 'Carte', mixte: 'Mixte', fiches: 'Fiches' }
const etatsBoutons = computed(() => (['carte', 'mixte', 'fiches'] as EtatVue[]).filter((e) => props.vf.cfg.etats.includes(e)))
// ── Pastilles actives (état Carte, et téléphone en Fiches) ──
const pastilles = computed(() => {
const out: { cle: string; libelle: string; variante: 'fonction' | 'echelle' | 'posture'; retirer: () => void }[] = []
for (const d of props.vf.cfg.dimensions) {
for (const v of props.vf.etat.filtres[d.cle] ?? []) {
const lib = d.valeurs.find((x) => x.valeur === v)?.libelle ?? v
out.push({ cle: `${d.cle}:${v}`, libelle: lib, variante: d.variante, retirer: () => props.vf.toggle(d.cle, v) })
}
}
if (props.vf.etat.sansAdresse) {
out.push({ cle: 'adresse', libelle: 'Sans adresse', variante: 'echelle', retirer: () => props.vf.setSansAdresse(false) })
}
return out
})
const libellesActifs = computed(() => {
const out: string[] = []
for (const d of props.vf.cfg.dimensions) {
for (const v of props.vf.etat.filtres[d.cle] ?? []) {
out.push(v, d.valeurs.find((x) => x.valeur === v)?.libelle ?? v)
}
}
return out
})
const resumeFiltres = computed(() => {
const parts = pastilles.value.map((p) => p.libelle)
if (props.vf.q) parts.unshift(`« ${props.vf.q} »`)
return parts.join(', ')
})
/** Chip cliquée dans une carte-fiche : ajoute le filtre, garde l'état de vue. */
function ajouterDepuisCarte(f: MiniFiche, libelle: string) {
const dim = props.vf.cfg.dimensions.find((d) => d.cle === f.chipsDimension)
if (!dim) return
const val = dim.valeurs.find((v) => v.libelle === libelle || v.valeur === libelle || v.valeur === libelle.replace(/^#/, ''))
if (val) props.vf.toggle(dim.cle, val.valeur)
}
function survoler(f: MiniFiche | null) {
props.vf.survolId = f ? f.id : null
emit('survol', f)
}
function choisirGroupe(e: Event) {
const v = (e.target as HTMLSelectElement).value
props.vf.setGroupe(v || null)
}
// ── Groupes repliables ──
const replies = ref(new Set<string>())
function basculerGroupe(cle: string) {
const s = new Set(replies.value)
s.has(cle) ? s.delete(cle) : s.add(cle)
replies.value = s
}
watch(() => props.vf.etat.groupe, () => { replies.value = new Set() })
const idGroupe = (cle: string) => (cle || 'sans').normalize('NFD').replace(/[^\w-]/g, '')
// ── Feuille « Filtrer » ──
const feuilleOuverte = ref(false)
const feuille = ref<HTMLElement | null>(null)
const boutonFiltrer = ref<HTMLElement | null>(null)
const boutonFermer = ref<HTMLElement | null>(null)
function ouvrirFeuille() {
feuilleOuverte.value = true
nextTick(() => boutonFermer.value?.focus())
}
function fermerFeuille() {
feuilleOuverte.value = false
nextTick(() => boutonFiltrer.value?.focus())
}
function pieger(e: KeyboardEvent) {
const el = feuille.value
if (!el) return
const f = [...el.querySelectorAll<HTMLElement>('button:not([disabled]), select, input')]
if (!f.length) return
const premier = f[0]
const dernier = f[f.length - 1]
if (e.shiftKey && document.activeElement === premier) { e.preventDefault(); dernier.focus() }
else if (!e.shiftKey && document.activeElement === dernier) { e.preventDefault(); premier.focus() }
}
// La feuille n'existe que sous 768 px : elle se ferme si l'écran s'élargit.
watch(() => props.vf.largeur, (l) => { if (l && l !== 'telephone') feuilleOuverte.value = false })
</script>
<style scoped>
/* ═══════════ Socle */
.vf {
--vf-liste: 320px;
display: flex;
flex-direction: column;
height: 100%;
min-height: 0;
min-width: 0;
background: var(--bg);
color: var(--text);
}
.vf-sr {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
/* ═══════════ Barre d'outils */
.vf-barre {
flex-shrink: 0;
display: flex;
flex-wrap: wrap;
align-items: center;
gap: var(--space-xs) var(--space-sm);
padding: var(--space-xs) var(--space-md);
background: var(--surface);
border-bottom: 1px solid var(--border);
min-width: 0;
}
.vf-seg {
display: inline-flex;
padding: 2px;
border-radius: var(--radius-md);
background: var(--bg-alt);
flex-shrink: 0;
}
.vf-seg__btn {
min-height: 32px;
padding: 0 var(--space-sm);
border: none;
border-radius: calc(var(--radius-md) - 2px);
background: none;
color: var(--text);
font: inherit;
font-size: var(--fs-sm);
font-weight: var(--fw-medium);
cursor: pointer;
}
.vf-seg__btn[aria-pressed='true'] {
background: var(--nav-primary-solid);
color: var(--nav-text-on-primary);
}
.vf-seg__n { display: none; }
.vf-recherche {
flex: 1 1 200px;
max-width: 340px;
min-width: 0;
display: flex;
align-items: center;
gap: var(--space-xs);
min-height: 36px;
padding: 0 var(--space-sm);
border: 1.5px solid var(--border);
border-radius: var(--radius-md);
background: var(--bg);
color: color-mix(in srgb, var(--text) 78%, transparent);
}
.vf-recherche:focus-within { border-color: var(--nav-primary-solid); }
.vf-recherche input {
flex: 1;
min-width: 0;
border: none;
outline: none;
background: transparent;
color: var(--text);
font: inherit;
font-size: var(--fs-sm);
}
.vf-recherche input::placeholder { color: color-mix(in srgb, var(--text) 70%, transparent); }
.vf-recherche input::-webkit-search-cancel-button { display: none; }
.vf-recherche__effacer {
display: flex;
padding: 4px;
border: none;
border-radius: var(--radius-full);
background: none;
color: inherit;
cursor: pointer;
}
.vf-filtrer {
display: none;
min-height: 36px;
padding: 0 var(--space-sm);
border: 1.5px solid var(--nav-primary-solid);
border-radius: var(--radius-md);
background: var(--surface);
color: var(--text);
font: inherit;
font-size: var(--fs-sm);
font-weight: var(--fw-semibold);
cursor: pointer;
flex-shrink: 0;
}
.vf-pastilles {
display: none;
flex-wrap: wrap;
gap: var(--space-2xs);
min-width: 0;
}
.vf-espace { flex: 1 1 0; min-width: 0; }
.vf-compteur {
margin: 0;
font-size: var(--fs-sm);
color: color-mix(in srgb, var(--text) 78%, transparent);
white-space: nowrap;
}
.vf-compteur__sep { margin: 0 var(--space-2xs); }
.vf-lien {
padding: 0;
border: none;
background: none;
color: var(--text);
font: inherit;
text-decoration: underline;
cursor: pointer;
}
.vf-select {
display: inline-flex;
align-items: center;
gap: var(--space-2xs);
font-size: var(--fs-sm);
color: color-mix(in srgb, var(--text) 78%, transparent);
flex-shrink: 0;
}
.vf-select select {
min-height: 32px;
padding: 0 var(--space-xs);
border: 1.5px solid var(--border);
border-radius: var(--radius-sm);
background: var(--bg);
color: var(--text);
font: inherit;
font-size: var(--fs-sm);
}
.vf-seg__btn:focus-visible,
.vf-filtrer:focus-visible,
.vf-lien:focus-visible,
.vf-recherche__effacer:focus-visible,
.vf-select select:focus-visible,
.vf-poignee:focus-visible,
.vf-groupe__bascule:focus-visible,
.vf-pied button:focus-visible {
outline: 2px solid var(--focus-ring);
outline-offset: 2px;
}
/* ═══════════ Corps */
.vf-corps {
position: relative;
flex: 1;
min-height: 0;
display: grid;
grid-template-columns: minmax(0, 1fr);
grid-template-rows: minmax(0, 1fr);
}
.vf-liste {
grid-column: 1;
grid-row: 1;
min-width: 0;
min-height: 0;
overflow-y: auto;
padding: var(--space-md);
background: var(--bg-alt);
}
.vf-intention {
margin: 0 0 var(--space-sm);
font-size: var(--fs-sm);
line-height: var(--lh-base);
color: color-mix(in srgb, var(--text) 78%, transparent);
}
.vf-filtres { margin-bottom: var(--space-md); }
.vf-etat-vide {
margin: var(--space-xl) 0;
text-align: center;
font-size: var(--fs-sm);
color: color-mix(in srgb, var(--text) 78%, transparent);
}
.vf-groupe + .vf-groupe { margin-top: var(--space-md); }
.vf-groupe__titre { margin: 0 0 var(--space-xs); font-size: var(--fs-sm); }
.vf-groupe__bascule {
display: inline-flex;
align-items: center;
gap: var(--space-xs);
min-height: 32px;
padding: 0;
border: none;
background: none;
color: var(--text);
font: inherit;
font-weight: var(--fw-semibold);
cursor: pointer;
}
.vf-groupe__chevron { display: inline-block; width: 1em; text-align: center; }
.vf-grille {
display: grid;
grid-template-columns: repeat(1, minmax(0, 1fr));
gap: var(--space-sm);
}
.vf-carte {
position: relative;
grid-column: 1;
grid-row: 1;
min-width: 0;
min-height: 0;
display: flex;
flex-direction: column;
}
.vf-poignee {
display: none;
position: absolute;
left: 0;
top: 50%;
z-index: 1001;
width: 22px;
height: 56px;
transform: translateY(-50%);
padding: 0;
border: 1px solid var(--border);
border-left: none;
border-radius: 0 var(--radius-md) var(--radius-md) 0;
background: var(--surface);
color: var(--text);
font-size: 1.1rem;
line-height: 1;
cursor: pointer;
box-shadow: var(--shadow-sm);
}
.vf-pied { display: none; }
/* ═══════════ État Fiches (et auto sous 1024 px) : la liste prend tout, la carte reste montée, invisible */
.vf--fiches .vf-carte,
.vf--auto .vf-carte {
grid-column: 1 / -1;
position: absolute;
inset: 0;
visibility: hidden;
pointer-events: none;
}
.vf--fiches .vf-grille,
.vf--auto .vf-grille {
grid-template-columns: repeat(1, minmax(0, 1fr));
}
@media (min-width: 768px) {
.vf--fiches .vf-grille,
.vf--auto .vf-grille { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (min-width: 1280px) {
.vf--fiches .vf-grille { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}
.vf--fiches .vf-select--carte,
.vf--auto .vf-select--carte { display: none; }
/* ═══════════ État Carte : la carte prend tout, liste repliée (invisible au clavier) */
.vf--carte .vf-liste { visibility: hidden; }
.vf--carte .vf-select--tri,
.vf--carte .vf-select--groupe { display: none; }
.vf--carte .vf-pastilles--barre { display: flex; }
/* ═══════════ Ordinateur (≥ 1024) : Mixte, poignée, transitions */
@media (min-width: 1024px) {
.vf-corps {
grid-template-columns: var(--vf-liste) minmax(0, 1fr);
transition: grid-template-columns var(--dur-base, 200ms) var(--ease-out, ease-out);
}
.vf-carte { grid-column: 2; }
.vf-poignee { display: block; }
.vf--fiches .vf-corps { grid-template-columns: minmax(0, 1fr) 0; }
.vf--fiches .vf-poignee { display: none; }
.vf--carte .vf-corps { --vf-liste: 0px; }
.vf--carte .vf-liste { padding: 0; overflow: hidden; }
.vf--mixte .vf-liste,
.vf--auto .vf-liste { border-right: 1px solid var(--border); background: var(--surface); }
.vf--mixte .vf-grille,
.vf--auto .vf-grille { grid-template-columns: repeat(1, minmax(0, 1fr)); gap: var(--space-xs); }
.vf--auto .vf-carte {
grid-column: 2;
position: relative;
inset: auto;
visibility: visible;
pointer-events: auto;
}
.vf--auto .vf-select--carte { display: inline-flex; }
}
@media (min-width: 1024px) and (prefers-reduced-motion: reduce) {
.vf-corps { transition: none; }
}
/* Mixte n'existe pas sous 1024 px */
@media (max-width: 1023.98px) {
.vf-seg__btn--mixte { display: none; }
.vf-carte, .vf-liste { grid-column: 1; }
}
/* ═══════════ Téléphone (< 768) */
@media (max-width: 767.98px) {
.vf-barre { padding: var(--space-xs) var(--space-sm); }
.vf-seg { flex: 1 1 100%; }
.vf-seg__btn { flex: 1; min-height: 40px; }
.vf-seg__n { display: inline; }
.vf-recherche { flex: 1 1 100px; }
.vf-espace { display: none; }
.vf-filtrer { display: inline-flex; align-items: center; }
.vf-filtres { display: none; }
.vf-compteur { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0, 0, 0, 0); }
/* compteur lu par les lecteurs d'écran seulement : rien de focalisable dedans */
.vf-compteur button, .vf-compteur__sep { display: none; }
.vf-select--groupe { display: none; }
.vf-select span:not(.vf-sr) { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0, 0, 0, 0); }
.vf-liste { padding: var(--space-sm); }
.vf--fiches .vf-pastilles--tel,
.vf--auto .vf-pastilles--tel {
display: flex;
padding: var(--space-xs) var(--space-sm);
background: var(--surface);
border-bottom: 1px solid var(--border);
}
/* Carte sur téléphone : segments + sous-vue en tête, pied en bas */
.vf--carte .vf-seg { flex: 1 1 0; }
.vf--carte .vf-recherche,
.vf--carte .vf-filtrer,
.vf--carte .vf-pastilles--barre,
.vf--carte .vf-espace { display: none; }
.vf--carte .vf-liste { display: none; }
.vf--carte .vf-pied {
display: flex;
align-items: center;
gap: var(--space-xs);
flex-shrink: 0;
padding: var(--space-xs) var(--space-sm);
background: var(--surface);
border-top: 1px solid var(--border);
}
}
.vf-pied__filtres {
flex: 1 1 auto;
min-width: 0;
display: inline-flex;
min-height: 40px;
align-items: center;
padding: 0 var(--space-sm);
border: 1.5px solid var(--border);
border-radius: var(--radius-md);
background: var(--bg);
color: var(--text);
font: inherit;
font-size: var(--fs-sm);
cursor: pointer;
}
.vf-pied__resume { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.vf-pied__voir {
margin-left: auto;
flex-shrink: 0;
min-height: 40px;
padding: 0 var(--space-sm);
border: none;
border-radius: var(--radius-md);
background: var(--nav-primary-solid);
color: var(--nav-text-on-primary);
font: inherit;
font-size: var(--fs-sm);
font-weight: var(--fw-semibold);
cursor: pointer;
}
/* ═══════════ Feuille « Filtrer » */
.vf-voile {
position: fixed;
inset: 0;
z-index: 10000;
background: rgba(17, 21, 32, 0.45);
}
.vf-feuille {
position: fixed;
left: 0;
right: 0;
bottom: 0;
z-index: 10001;
display: flex;
flex-direction: column;
max-height: 85vh;
border-radius: var(--radius-lg, 16px) var(--radius-lg, 16px) 0 0;
background: var(--surface);
color: var(--text);
box-shadow: 0 -8px 32px rgba(17, 21, 32, 0.25);
}
.vf-feuille__tete {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--space-sm) var(--space-md);
border-bottom: 1px solid var(--border);
}
.vf-feuille__tete h2 { margin: 0; font-size: var(--fs-md, 1rem); }
.vf-feuille__fermer {
width: 44px;
height: 44px;
border: none;
background: none;
color: var(--text);
font-size: 1.1rem;
cursor: pointer;
}
.vf-feuille__corps {
flex: 1;
min-height: 0;
overflow-y: auto;
padding: var(--space-md);
display: flex;
flex-direction: column;
gap: var(--space-md);
}
.vf-feuille__pied {
padding: var(--space-sm) var(--space-md);
border-top: 1px solid var(--border);
}
.vf-feuille__voir {
width: 100%;
min-height: 44px;
border: none;
border-radius: var(--radius-md);
background: var(--nav-primary-solid);
color: var(--nav-text-on-primary);
font: inherit;
font-weight: var(--fw-semibold);
cursor: pointer;
}
.vf-feuille__fermer:focus-visible,
.vf-feuille__voir:focus-visible {
outline: 2px solid var(--focus-ring);
outline-offset: 2px;
}
</style>