{# # ============================================================================ # MOLECULE: FILTER TABS # ============================================================================ # # Groupe de boutons filtres style Moodia. # Conteneur pill avec fond blanc et filtres arrondis. # # ============================================================================ # COMPOSITION (Atomes utilisés) # ============================================================================ # # - Aucun (composant autonome) # # ============================================================================ # PARAMÈTRES # ============================================================================ # # | Paramètre | Type | Défaut | Description | # |---------------|---------|-----------|-----------------------------------| # | filters | array | (requis) | Liste des filtres | # | └ id | string | (requis) | Identifiant du filtre | # | └ label | string | (requis) | Texte du filtre | # | └ count | number | null | Compteur | # | └ href | string | null | URL (si lien au lieu de bouton) | # | └ icon | string | null | Nom d'icône (atome icon) | # | └ attr | object | {} | Attributs HTML additionnels | # | active | string | premier | ID du filtre actif | # | size | string | 'md' | sm, md, lg | # | name | string | 'filter' | Nom pour le data-attribute | # | clientToggle | bool | false | État actif basculé par le JS (.active) | # | class | string | '' | Classes additionnelles | # # ============================================================================ # EXEMPLES D'UTILISATION # ============================================================================ # # Filtres de type: # {% include '_molecules/filter-tabs/filter-tabs.html.twig' with { # filters: [ # { id: 'all', label: 'Tous', count: 40 }, # { id: 'course', label: 'Cours', count: 10 }, # { id: 'course_quiz', label: 'Cours + Quiz', count: 29 }, # { id: 'quiz_only', label: 'Quiz', count: 1 } # ], # active: 'all' # } %} # # ============================================================================ #} {# ============================================================================ CONFIGURATION DES VALEURS PAR DÉFAUT ============================================================================ #} {% set filters = filters ?? [] %} {% set active = active ?? (filters|first).id ?? null %} {% set size = size ?? 'md' %} {% set name = name ?? 'filter' %} {% set class = class ?? '' %} {# `clientToggle` : l'état actif est basculé par le JS après le rendu. Les styles passent alors en variantes `[&.active]:` pour que le script n'ait qu'UNE classe à ajouter ou retirer — lui faire manipuler la liste complète des classes du bouton actif le rendrait solidaire du détail visuel de cette molécule. #} {% set clientToggle = clientToggle ?? false %} {# ============================================================================ CLASSES TAILWIND ============================================================================ #} {% set sizeConfig = { 'sm': { 'padding': 'px-3 py-1.5', 'text': 'text-xs', 'gap': 'gap-1.5', 'containerPadding': 'p-1', 'containerGap': 'gap-1', 'countPadding': 'px-1.5 py-0.5', 'countText': 'text-[10px]' }, 'md': { 'padding': 'px-4 py-2.5', 'text': 'text-sm', 'gap': 'gap-2', 'containerPadding': 'p-1.5', 'containerGap': 'gap-1.5', 'countPadding': 'px-2 py-0.5', 'countText': 'text-[11px]' }, 'lg': { 'padding': 'px-5 py-3', 'text': 'text-base', 'gap': 'gap-2.5', 'containerPadding': 'p-2', 'containerGap': 'gap-2', 'countPadding': 'px-2.5 py-1', 'countText': 'text-xs' } } %} {% set sConfig = sizeConfig[size] ?? sizeConfig['md'] %} {# ============================================================================ RENDU HTML ============================================================================ #}
{% for filter in filters %} {% set isActive = filter.id == active %} {% set tag = filter.href is defined and filter.href ? 'a' : 'button' %} <{{ tag }} {% if tag == 'a' %}href="{{ filter.href }}"{% else %}type="button"{% endif %} data-filter-id="{{ filter.id }}" {% if filter.attr is defined %}{% for key, val in filter.attr %}{{ key }}="{{ val }}" {% endfor %}{% endif %} class="flex sm:inline-flex items-center justify-between sm:justify-start {{ sConfig.padding }} {{ sConfig.text }} {{ sConfig.gap }} font-medium rounded-xl sm:rounded-full transition-all duration-250 cursor-pointer outline-none {% if clientToggle %} text-gray-600 hover:text-primary hover:bg-primary/[0.06] [&.active]:text-white [&.active]:bg-gradient-to-r [&.active]:from-primary [&.active]:to-primary-light [&.active]:shadow-primary {{ isActive ? 'active' : '' }} {% elseif isActive %} text-white bg-gradient-to-r from-primary to-primary-light shadow-[0_3px_12px_rgba(12,129,228,0.3)] {% else %} text-gray-600 hover:text-primary hover:bg-primary/[0.06] {% endif %}" {% if clientToggle %}aria-pressed="{{ isActive ? 'true' : 'false' }}"{% elseif isActive %}aria-pressed="true"{% endif %} > {% if filter.icon is defined and filter.icon %} {% include '_atoms/icon/icon.html.twig' with {name: filter.icon, size: 'xs', color: 'current'} only %} {% endif %} {{ filter.label }} {% if filter.count is defined and filter.count is not null %} {# data-filter-count : point d'accroche stable pour le JS qui ajuste un compteur sans rechargement (cf. scormia--library). Sans lui, il faudrait viser « le dernier span du bouton » — muet le jour où l'ordre change. #} {{ filter.count }} {% endif %} {% endfor %}