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
813 lines
25 KiB
Vue
813 lines
25 KiB
Vue
<!--
|
||
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>
|