- components/Chip.vue (nouveau) : label/variant(echelle|fonction|posture)/
active/count/as(button|span)/removable, emits toggle+remove, aria-pressed
natif sur as=button, croix "Retirer {label}" accessible. Styles 100% tokens
DS (--primary/--on-primary/--bg-alt/--accent/--on-accent/--focus-ring/
--dur-fast), hover translateY, focus-visible outline var(--focus-ring),
transition coupée sous prefers-reduced-motion. Taille compacte 32px de haut
mesurée (tap ok).
- components/ChipGroup.vue (nouveau) : role="group" + aria-label + flex-wrap
gap — garantit le wrap propre exigé (jamais une ligne à virgules).
- EchelleFilter.vue, FonctionFilter.vue : rendu remplacé par Chip/ChipGroup,
props modelValue/counts et événement update:modelValue strictement
inchangés (NavSidebar.vue n'a rien à changer côté logique). FonctionFilter
passe le rang de sélection (comme avant) via le prop count de Chip.
- HashtagFilter.vue : seule la liste HASHTAGS (togglable, grammaire fonction)
passe en Chip/ChipGroup. Les chips FAMILLES gardent leur rendu et leurs
couleurs propres par famille (hors grammaire échelle/fonction/posture,
hors périmètre B3 — les changer aurait été une refonte, pas un branchement).
- pages/agences.vue : hashtags par structure (liste sidebar) en
Chip as="span" variant="fonction" (non cliquables avant, le restent).
- pages/index.vue : la sheet mobile ne passe PAS par NavSidebar/EchelleFilter/
FonctionFilter — elle a son propre bloc de chips dupliqué (découvert en
testant le mobile, absent du périmètre écrit du brief). Direction du parti
design ("chips codées par dimension", mobile-first explicite) : convertie
en Chip/ChipGroup elle aussi, mêmes handlers toggleEchelle/toggleFonction
inchangés. Effet de bord positif : le nombre par fonction (déjà calculé,
jamais affiché sur mobile) apparaît désormais aussi en mobile, à parité
avec le desktop.
- components/TagBadge.vue supprimé : son seul consommateur, OrgCard.vue,
n'est importé nulle part dans l'app (grep vérifié) — code mort. OrgCard.vue
supprimé avec lui plutôt que laissé avec une référence à un composant
disparu.
- components/TypeBadge.vue : conservé tel quel — ce n'est pas un filtre, et
sa grammaire couleur par type (7 couleurs Tailwind fixes) est orthogonale
à échelle/fonction/posture ; le convertir en Chip aurait perdu l'info de
type ou inventé une 4e variante non validée.
Vérifié : npm run build OK. Chips wrap avec gap en clair/sombre/mobile
(390px, hauteur mesurée 32px). Dark mode : contraste texte/fond conforme
(safran = texte sombre, jamais blanc).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
171 lines
4.0 KiB
Vue
171 lines
4.0 KiB
Vue
<template>
|
|
<component
|
|
:is="as"
|
|
:type="as === 'button' ? 'button' : undefined"
|
|
class="chip"
|
|
:class="[`chip--${variant}`, { 'chip--active': active }]"
|
|
:aria-pressed="as === 'button' ? active : undefined"
|
|
@click="as === 'button' && emit('toggle')"
|
|
>
|
|
<span class="chip__label">{{ label }}</span>
|
|
<span v-if="count !== undefined" class="chip__count">{{ count }}</span>
|
|
<button
|
|
v-if="removable"
|
|
type="button"
|
|
class="chip__remove"
|
|
:aria-label="`Retirer ${label}`"
|
|
@click.stop="emit('remove')"
|
|
>
|
|
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round">
|
|
<line x1="18" y1="6" x2="6" y2="18" />
|
|
<line x1="6" y1="6" x2="18" y2="18" />
|
|
</svg>
|
|
</button>
|
|
</component>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
withDefaults(
|
|
defineProps<{
|
|
label: string
|
|
variant?: 'echelle' | 'fonction' | 'posture'
|
|
active?: boolean
|
|
count?: number
|
|
as?: 'button' | 'span'
|
|
removable?: boolean
|
|
}>(),
|
|
{
|
|
variant: 'fonction',
|
|
active: false,
|
|
count: undefined,
|
|
as: 'button',
|
|
removable: false,
|
|
}
|
|
)
|
|
|
|
const emit = defineEmits<{
|
|
toggle: []
|
|
remove: []
|
|
}>()
|
|
</script>
|
|
|
|
<style scoped>
|
|
.chip {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: var(--space-2xs);
|
|
padding: var(--space-2xs) var(--space-sm);
|
|
border-radius: var(--radius-full);
|
|
font-size: var(--fs-xs);
|
|
line-height: var(--lh-tight);
|
|
font-weight: var(--fw-medium);
|
|
border: 1.5px solid transparent;
|
|
cursor: pointer;
|
|
user-select: none;
|
|
min-height: 32px;
|
|
box-sizing: border-box;
|
|
background: none;
|
|
transition: background-color var(--dur-fast) var(--ease-out),
|
|
color var(--dur-fast) var(--ease-out),
|
|
border-color var(--dur-fast) var(--ease-out),
|
|
transform var(--dur-fast) var(--ease-out);
|
|
}
|
|
|
|
@media (prefers-reduced-motion: reduce) {
|
|
.chip {
|
|
transition: none;
|
|
}
|
|
}
|
|
|
|
.chip:hover {
|
|
transform: translateY(-1px);
|
|
}
|
|
|
|
.chip:focus-visible {
|
|
outline: 2px solid var(--focus-ring);
|
|
outline-offset: 2px;
|
|
}
|
|
|
|
/* ── Échelle : outline bleu, fond transparent, actif = plein ────────────── */
|
|
.chip--echelle {
|
|
border-color: var(--primary);
|
|
color: var(--text);
|
|
background: transparent;
|
|
}
|
|
.chip--echelle:hover {
|
|
background: var(--primary-hover);
|
|
border-color: var(--primary-hover);
|
|
color: var(--on-primary);
|
|
}
|
|
.chip--echelle.chip--active {
|
|
background: var(--primary);
|
|
border-color: var(--primary);
|
|
color: var(--on-primary);
|
|
}
|
|
.chip--echelle.chip--active:hover {
|
|
background: var(--primary-hover);
|
|
border-color: var(--primary-hover);
|
|
}
|
|
|
|
/* ── Fonction/hashtag : pleine bleu, inactif = bg-alt ────────────────────── */
|
|
.chip--fonction {
|
|
border-color: transparent;
|
|
background: var(--bg-alt);
|
|
color: var(--text);
|
|
}
|
|
.chip--fonction:hover {
|
|
background: var(--primary-hover);
|
|
color: var(--on-primary);
|
|
}
|
|
.chip--fonction.chip--active {
|
|
background: var(--primary);
|
|
color: var(--on-primary);
|
|
}
|
|
.chip--fonction.chip--active:hover {
|
|
background: var(--primary-hover);
|
|
}
|
|
|
|
/* ── Posture (préparée pour plus tard) : outline safran ──────────────────── */
|
|
.chip--posture {
|
|
border-color: var(--accent);
|
|
color: var(--text);
|
|
background: transparent;
|
|
}
|
|
.chip--posture:hover {
|
|
background: var(--accent);
|
|
color: var(--on-accent);
|
|
}
|
|
.chip--posture.chip--active {
|
|
background: var(--accent);
|
|
border-color: var(--accent);
|
|
color: var(--on-accent);
|
|
}
|
|
|
|
.chip__count {
|
|
font-size: 0.85em;
|
|
opacity: 0.75;
|
|
}
|
|
|
|
.chip__remove {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 14px;
|
|
height: 14px;
|
|
padding: 0;
|
|
border: none;
|
|
background: transparent;
|
|
color: inherit;
|
|
opacity: 0.7;
|
|
cursor: pointer;
|
|
border-radius: var(--radius-full);
|
|
}
|
|
.chip__remove:hover {
|
|
opacity: 1;
|
|
}
|
|
.chip__remove:focus-visible {
|
|
outline: 2px solid var(--focus-ring);
|
|
outline-offset: 1px;
|
|
}
|
|
</style>
|