feat(vue-fiches): composant VueFiches (Carte · Mixte · Fiches, poignée, barre d'outils) et filtres par dimension (B11-M3, M4)

Filtres à un seul endroit par état, chips à 0 grisées, repli au-delà de 14,
feuille « Filtrer » sous 768 px, pastilles actives en état Carte.
FicheMiniCard : racine <article> + bouton étiré pour rendre les chips cliquables.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SCt6e8wWiYqRXLJAuKqP5i
This commit is contained in:
Jules Neny
2026-10-02 17:57:27 +02:00
co-authored by Claude Opus 5.5
parent 38df1d5eb5
commit 5a4c7af79c
9 changed files with 1011 additions and 844 deletions
+812
View File
@@ -0,0 +1,812 @@
<!--
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); }
.vf-select--groupe { display: none; }
.vf-select span:not(.vf-sr) { display: none; }
.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>