From 0cfafedeef3c99343e727e254d3f5ccd12beba08 Mon Sep 17 00:00:00 2001 From: Colin Maudry Date: Tue, 30 Jun 2026 13:21:24 +0200 Subject: [PATCH] feat: charte graphique des boutons (primary/secondary/danger) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Override CSS scopé aux classes .btn-* (jamais les variables --bs-* racine, donc alertes/badges inchangés) pour remplacer les couleurs dérivées du thème Simplex (danger mauve, secondary gris illisible). Trois rôles : la couleur encode la fonction, le remplissage l'emphase. - primary terracotta : action principale validante - secondary gris ardoise : action neutre/alternative (lisible) - danger rouge #c0392b : destructif (outline par défaut, plein en confirmation) Audit + conformation des usages : - compte_admin « Supprimer mon compte » : primary outline -> danger outline - compte_abonnement « Me désabonner » : outline-primary -> outline-danger - compte_abonnement « Je suis sûr » : outline-primary -> danger plein Tests : ajout tests/test_boutons.py (garde de non-régression du primary). conftest : implémente le hook pytest_setup_options (--headless=new) ; la fixture chrome_options n'était jamais utilisée par dash.testing, d'où les fenêtres Chrome qui s'ouvraient pendant les tests. Specs et plan : docs/superpowers/{specs,plans}/2026-06-30-charte-boutons*. Co-Authored-By: Claude Opus 4.8 --- .../plans/2026-06-30-charte-boutons.md | 302 ++++++++++++++++++ .../specs/2026-06-30-charte-boutons-design.md | 124 +++++++ src/assets/css/style.css | 111 ++++++- src/pages/compte_abonnement.py | 4 +- src/pages/compte_admin.py | 2 +- tests/conftest.py | 24 +- tests/test_boutons.py | 25 ++ 7 files changed, 567 insertions(+), 25 deletions(-) create mode 100644 docs/superpowers/plans/2026-06-30-charte-boutons.md create mode 100644 docs/superpowers/specs/2026-06-30-charte-boutons-design.md create mode 100644 tests/test_boutons.py diff --git a/docs/superpowers/plans/2026-06-30-charte-boutons.md b/docs/superpowers/plans/2026-06-30-charte-boutons.md new file mode 100644 index 0000000..436c5a2 --- /dev/null +++ b/docs/superpowers/plans/2026-06-30-charte-boutons.md @@ -0,0 +1,302 @@ +# Charte graphique des boutons — Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Remplacer les couleurs de boutons dérivées du thème Simplex (danger mauve, secondary gris illisible) par une charte à trois rôles — `primary` terracotta, `secondary` gris ardoise, `danger` rouge — où la couleur encode la fonction et le remplissage l'emphase. + +**Architecture:** Override CSS ciblant exclusivement les classes `.btn-*` dans `src/assets/css/style.css` (jamais les variables `--bs-*` racine, pour ne pas affecter alertes/badges). Le code applicatif continue d'utiliser `color="primary|secondary|danger"` + `outline=True|False` natifs de dash-bootstrap-components. Puis audit des boutons existants pour conformer leur usage à la charte. + +**Tech Stack:** Dash 3.4, dash-bootstrap-components, Bootstrap 5 (thème Simplex), CSS, pytest + Selenium (DashComposite). + +## Global Constraints + +- Override **uniquement** les sélecteurs `.btn-*` ; ne **jamais** modifier les variables racine `--bs-primary`, `--bs-danger`, `--bs-secondary` (alertes/badges doivent rester inchangés). Source : spec « Hors périmètre ». +- Imports applicatifs toujours préfixés `src.` (ex. `src.pages.recherche`). +- Palette exacte : terracotta `#b33821`, danger rouge `#c0392b`, secondary texte `#344054` / bord `#5a6570`, disabled bord `#ccc` / texte `#666`. +- `border-radius: 3px` et typographie `Inter` poids 400 conservés sur les boutons. +- Focus clavier visible (`:focus-visible`) sur tous les boutons restylés (accessibilité). +- Respecter `prefers-reduced-motion` pour toute transition de survol. +- Tests : `rtk pytest` (Selenium, nécessite Chrome/Chromium). `DEVELOPMENT=true` est positionné automatiquement. + +--- + +### Task 1: Override CSS des trois rôles de boutons + +Redéfinit l'apparence des classes `.btn-primary`, `.btn-outline-primary`, `.btn-secondary`, `.btn-outline-secondary`, `.btn-danger`, `.btn-outline-danger` dans la feuille de style applicative, en conservant l'API dbc native. Ajoute un test Selenium de non-régression vérifiant que la feuille est bien appliquée au bouton primaire de la page d'accueil (publique). + +**Files:** + +- Modify: `src/assets/css/style.css` (bloc « Base Button Styles » autour des lignes 42-79) +- Test: `tests/test_boutons.py` (créer) + +**Interfaces:** + +- Consumes: rien (première tâche). +- Produces: classes CSS restylées `.btn-primary`, `.btn-outline-primary`, `.btn-secondary`, `.btn-outline-secondary`, `.btn-danger`, `.btn-outline-danger`. Aucun symbole Python. + +- [ ] **Step 1: Écrire le test de non-régression (échoue d'abord car couleur cible non garantie)** + +Créer `tests/test_boutons.py`. Le bouton « Rechercher » de la page d'accueil (`/`, public, `className="btn btn-primary"` dans `src/pages/recherche.py:56`) doit être rendu terracotta plein (le dégradé existant a `rgb(179, 56, 33)` en couleur médiane → on vérifie une composante rouge dominante et un fond non transparent). + +```python +from dash.testing.composite import DashComposite + + +def _rgb_tuple(css_color: str) -> tuple[int, int, int]: + """Parse 'rgb(r, g, b)' ou 'rgba(r, g, b, a)' en (r, g, b).""" + inner = css_color[css_color.index("(") + 1 : css_color.index(")")] + parts = [p.strip() for p in inner.split(",")] + return (int(parts[0]), int(parts[1]), int(parts[2])) + + +def test_btn_primary_is_terracotta(dash_duo: DashComposite): + from src.app import app + + dash_duo.start_server(app) + dash_duo.wait_for_element("a.btn.btn-primary, button.btn.btn-primary", timeout=10) + btn = dash_duo.find_element("a.btn.btn-primary, button.btn.btn-primary") + + # Le dégradé terracotta est posé via background-image ; la couleur de + # repli background-color ne doit pas être le bleu Bootstrap par défaut. + bg_image = btn.value_of_css_property("background-image") + color = btn.value_of_css_property("color") + + assert "gradient" in bg_image # dégradé terracotta appliqué + r, g, b = _rgb_tuple(color) + assert (r, g, b) == (255, 255, 255) # texte blanc +``` + +- [ ] **Step 2: Lancer le test pour vérifier l'état initial** + +Run: `rtk pytest tests/test_boutons.py::test_btn_primary_is_terracotta -v` +Expected: PASS si le `.btn-primary` actuel s'applique déjà (dégradé + texte blanc déjà présents lignes 51-65). Ce test sert de **garde de non-régression** : il doit rester vert après le refactor CSS. S'il échoue ici, c'est que la feuille n'est pas chargée → investiguer avant de continuer. + +- [ ] **Step 3: Réécrire le bloc boutons dans `style.css`** + +Remplacer le bloc commenté/partiel « Base Button Styles » (lignes ~42-79 : le commentaire mort, `button.btn.btn-primary, button.show-hide { … }`, son `:hover`, et `button[disabled]`) par la charte complète ci-dessous. Conserver le dégradé terracotta existant pour `.btn-primary` et le `button.show-hide` (qui réutilise ce style). + +```css +/* ========================================================================== + Boutons — charte à 3 rôles (couleur = fonction, remplissage = emphase) + Voir docs/superpowers/specs/2026-06-30-charte-boutons-design.md + Override scopé aux classes .btn-* uniquement (n'affecte pas les alertes). + ========================================================================== */ + +:root { + --btn-terracotta: #b33821; + --btn-terracotta-text: #fff; + --btn-secondary-text: #344054; + --btn-secondary-border: #5a6570; + --btn-secondary-hover-bg: #f1f3f5; + --btn-danger: #c0392b; + --btn-danger-text: #fff; +} + +/* Base commune : rayon + typo */ +.btn { + border-radius: 3px; + font-family: "Inter", sans-serif; + font-weight: 400; +} + +/* --- PRIMARY plein : action principale validante (1 par contexte) --- */ +button.btn.btn-primary, +a.btn.btn-primary, +button.show-hide { + display: block; + outline: 0; + color: var(--btn-terracotta-text); + border: 0; + background-image: linear-gradient( + rgb(209, 96, 73), + rgb(179, 56, 33) 26%, + rgb(159, 36, 22) + ); +} + +button.btn.btn-primary:hover, +a.btn.btn-primary:hover, +button.show-hide:hover { + color: var(--btn-terracotta-text); + background-image: linear-gradient( + rgb(239, 126, 103), + rgb(209, 86, 63) 26%, + rgb(189, 66, 52) + ); +} + +/* --- PRIMARY outline : action affirmative de moindre emphase --- */ +.btn.btn-outline-primary { + color: var(--btn-terracotta); + border: 1px solid var(--btn-terracotta); + background-color: transparent; + background-image: none; +} + +.btn.btn-outline-primary:hover, +.btn.btn-outline-primary:focus-visible { + color: var(--btn-terracotta-text); + background-color: var(--btn-terracotta); + border-color: var(--btn-terracotta); +} + +/* --- SECONDARY : action neutre / alternative (gris ardoise) --- */ +.btn.btn-secondary, +.btn.btn-outline-secondary { + color: var(--btn-secondary-text); + border: 1px solid var(--btn-secondary-border); + background-color: transparent; + background-image: none; +} + +.btn.btn-secondary:hover, +.btn.btn-secondary:focus-visible, +.btn.btn-outline-secondary:hover, +.btn.btn-outline-secondary:focus-visible { + color: var(--btn-secondary-text); + background-color: var(--btn-secondary-hover-bg); + border-color: var(--btn-secondary-border); +} + +/* --- DANGER outline : action destructive dans le flux courant --- */ +.btn.btn-outline-danger { + color: var(--btn-danger); + border: 1px solid var(--btn-danger); + background-color: transparent; + background-image: none; +} + +.btn.btn-outline-danger:hover, +.btn.btn-outline-danger:focus-visible { + color: var(--btn-danger-text); + background-color: var(--btn-danger); + border-color: var(--btn-danger); +} + +/* --- DANGER plein : confirmation finale destructive (modale) --- */ +.btn.btn-danger { + color: var(--btn-danger-text); + border: 1px solid var(--btn-danger); + background-color: var(--btn-danger); + background-image: none; +} + +.btn.btn-danger:hover, +.btn.btn-danger:focus-visible { + color: var(--btn-danger-text); + background-color: #a93226; + border-color: #a93226; +} + +/* --- État désactivé commun (conserve l'existant) --- */ +.btn[disabled], +button[disabled] { + border-color: #ccc; + color: #666; + background-image: none; +} + +@media (prefers-reduced-motion: no-preference) { + .btn { + transition: background-color 0.15s ease, color 0.15s ease, + border-color 0.15s ease; + } +} +``` + +- [ ] **Step 4: Relancer le test de non-régression** + +Run: `rtk pytest tests/test_boutons.py::test_btn_primary_is_terracotta -v` +Expected: PASS (le `.btn-primary` conserve son dégradé terracotta et son texte blanc). + +- [ ] **Step 5: Vérifier l'absence de régression sur la suite complète** + +Run: `rtk pytest` +Expected: PASS (aucune régression introduite par le CSS). + +- [ ] **Step 6: Vérification visuelle des trois rôles** + +Lancer l'app (`python run.py`), se connecter, ouvrir `/compte/vues`. Vérifier : + +- « Renommer » : gris ardoise lisible (texte `#344054`, bord `#5a6570`), plus de gris clair illisible ; +- « Supprimer » : rouge `#c0392b` en outline, plus aucun mauve ; au survol, plein rouge texte blanc ; +- focus clavier (Tab) : anneau/fond visible sur chaque bouton. + Prendre une capture pour comparaison avec la capture initiale. + +- [ ] **Step 7: Commit** (différé — voir note d'intégration en fin de plan : spec + plan + code committés ensemble) + +--- + +### Task 2: Audit et conformation des boutons existants + +Passe en revue les ~46 occurrences `color=…` et 8 `outline=True` pour s'assurer qu'elles respectent la charte : une seule action `primary` pleine par contexte, destructif outline par défaut sauf confirmation finale en modale (plein rouge), neutre en `secondary` outline. + +**Files:** + +- Inspect: `src/saved_views/ui.py`, `src/pages/compte_vues.py`, `src/pages/compte_abonnement.py`, `src/pages/compte_abonnement_mes_infos.py`, `src/pages/mot_de_passe_oublie.py`, `src/pages/recherche.py`, `src/pages/tableau.py`, `src/app.py` +- Modify: uniquement les fichiers dont un bouton enfreint la charte (à déterminer pendant l'audit) +- Test: `tests/test_boutons.py` (réutilise la garde de non-régression de Task 1) + +**Interfaces:** + +- Consumes: classes CSS restylées de Task 1. +- Produces: aucun symbole ; usages de boutons conformes à la charte. + +- [ ] **Step 1: Recenser les boutons et leur rôle** + +Run: `grep -rn 'dbc.Button\|className="btn' src/ --include=*.py` +Pour chaque bouton, noter (page, libellé, `color`, `outline`) et confronter à la charte : + +| Cas | Attendu | +| ------------------------------------------------------------------------------------- | --------------------------------- | +| Action principale d'un écran/formulaire (Rechercher, Enregistrer, Envoyer, S'abonner) | `color="primary"` plein | +| Déclencheur destructif dans une liste (Supprimer, Se désabonner) | `color="danger", outline=True` | +| Confirmation destructive finale en modale | `color="danger"` plein | +| Action neutre (Renommer, Annuler, navigation) | `color="secondary", outline=True` | + +Vérifier en particulier : aucun écran ne doit présenter **deux** boutons `primary` pleins en concurrence. + +- [ ] **Step 2: Corriger les boutons non conformes** + +Éditer uniquement les boutons divergents. Exemple de correction type (n'appliquer que si un cas réel est trouvé) : + +```python +# Avant — déclencheur de suppression en plein rouge dans une liste +dbc.Button("Supprimer", id={"type": "vue-delete", "index": vid}, color="danger") +# Après — outline par défaut hors confirmation finale +dbc.Button("Supprimer", id={"type": "vue-delete", "index": vid}, color="danger", outline=True) +``` + +Si l'audit ne révèle aucune divergence (les usages de `src/saved_views/ui.py` sont déjà conformes : Renommer = `secondary`+outline, Supprimer = `danger`+outline ; la confirmation modale `vue-rename-confirm` = `primary`), documenter « aucun changement nécessaire » dans le message de commit et passer au Step 3. + +- [ ] **Step 3: Vérifier la non-régression** + +Run: `rtk pytest` +Expected: PASS. + +- [ ] **Step 4: Vérification visuelle multi-pages** + +Lancer l'app, parcourir `/compte/vues`, `/compte/abonnement`, `/` et `/tableau`. Confirmer que chaque bouton respecte son rôle et qu'il n'y a pas deux `primary` pleins concurrents par écran. + +- [ ] **Step 5: Commit** (différé — voir note d'intégration ci-dessous) + +--- + +## Note d'intégration + +Sur demande explicite de l'utilisateur, le **spec + le plan + le code** sont +committés **ensemble** (pas de commit intermédiaire). Après validation des deux +tâches, faire un unique commit : + +```bash +git add docs/superpowers/specs/2026-06-30-charte-boutons-design.md \ + docs/superpowers/plans/2026-06-30-charte-boutons.md \ + src/assets/css/style.css tests/test_boutons.py +# + tout fichier page modifié pendant l'audit (Task 2) +git commit -m "feat: charte graphique des boutons (primary/secondary/danger) # + +Co-Authored-By: Claude Opus 4.8 " +``` + +Branche courante : `dev` (auto-déploie sur test.decp.info). Ne pas pousser sans +demande explicite. diff --git a/docs/superpowers/specs/2026-06-30-charte-boutons-design.md b/docs/superpowers/specs/2026-06-30-charte-boutons-design.md new file mode 100644 index 0000000..a1bd91e --- /dev/null +++ b/docs/superpowers/specs/2026-06-30-charte-boutons-design.md @@ -0,0 +1,124 @@ +# Charte graphique des boutons — design + +Date : 2026-06-30 +Statut : validé (charte), à implémenter + +## Problème + +Le thème Bootstrap **Simplex** dérive ses couleurs contextuelles à partir de la +couleur primaire. La primaire de decp.info étant un terracotta chaud +(`rgb(179, 56, 33)` = `#b33821`), Simplex calcule un **`danger` mauve** (`#9b479f`) +et un **`secondary` gris très clair**. Résultat constaté sur `/compte/vues` : + +- bouton **Renommer** (`color="secondary", outline=True`) : gris clair sur fond + blanc, quasi illisible ; +- bouton **Supprimer** (`color="danger", outline=True`) : mauve, sans rapport + sémantique avec une action destructive. + +Les couleurs Simplex ne servent donc pas la lisibilité ni la sémantique. Il faut +**redéfinir les styles `primary`, `secondary` et `danger`** des boutons et établir +une charte claire où **chaque style a une fonction**. + +## Principe directeur + +**La couleur encode la fonction (sémantique) ; le remplissage encode l'emphase.** + +Trois rôles seulement. Le code applicatif continue d'écrire +`color="primary|secondary|danger"` + `outline=True|False` exactement comme avant — +le CSS ne fait que restyler ces classes. Aucun changement d'API, aucun helper. + +## La charte + +| Style (dbc) | Fonction | Apparence | +| ----------------------- | ------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------ | +| `primary` plein | **L'**action principale validante d'un contexte — idéalement une par écran (Rechercher, Enregistrer, S'abonner, Envoyer) | terracotta plein `#b33821`, texte blanc (dégradé existant conservé) | +| `primary` + `outline` | action affirmative de moindre emphase (quand le plein serait trop lourd) | bord + texte terracotta, fond transparent ; survol = plein terracotta texte blanc | +| `secondary` + `outline` | action **neutre / alternative** (Renommer, Annuler, navigation) | gris ardoise — bord `#5a6570`, texte `#344054`, fond transparent ; survol = fond gris très clair | +| `danger` + `outline` | action **destructive** dans une liste ou le flux courant (Supprimer, Se désabonner) | rouge `#c0392b` — bord + texte, fond transparent ; survol = plein rouge texte blanc | +| `danger` plein | **confirmation finale** destructive (dans une modale) | rouge plein `#c0392b`, texte blanc | + +### Règles d'usage + +1. **Une seule action `primary` pleine par contexte.** Tout le reste est `outline` + ou `secondary`. +2. **Le destructif est `outline` par défaut** ; seul le bouton de confirmation + finale (modale) est plein rouge. +3. **`secondary` est le défaut neutre** — remplace les boutons gris clair + illisibles sur fond blanc. + +## Palette + +Quatre valeurs ajoutées/confirmées ; le reste hérité de Simplex. + +``` +terracotta #b33821 primary (chaud, marque) — existant +ardoise texte #344054 / bord #5a6570 secondary (froid, neutre) +rouge #c0392b danger (chaud saturé, distinct du terracotta) +disabled bord #ccc / texte #666 état désactivé — existant, conservé +``` + +Le rouge `#c0392b` est volontairement plus saturé et légèrement plus froid que le +terracotta `#b33821`, pour rester distinguable à côté d'un bouton primaire. + +## Périmètre + +### Dans le périmètre + +- Override CSS des classes **boutons uniquement** dans `src/assets/css/style.css` : + - `.btn-primary` (harmonisation du dégradé déjà présent) + - `.btn-outline-primary` + - `.btn-secondary` et `.btn-outline-secondary` + - `.btn-danger` et `.btn-outline-danger` + - états `:hover`, `:focus-visible`, `:disabled` correspondants +- Audit des boutons existants (~46 occurrences `color=…`, dont 8 `outline=True`) + pour corriger les cas qui ne respectent pas la charte, en particulier : + - `src/saved_views/ui.py` : Renommer (`secondary`+outline), Supprimer + (`danger`+outline) — déjà conformes en intention, à revalider visuellement ; + - vérifier qu'aucun écran n'a deux `primary` pleins concurrents ; + - vérifier que les confirmations destructives en modale sont en `danger` plein + et les déclencheurs en liste en `danger` outline. + +### Hors périmètre (inchangé) + +- Les variables racine `--bs-primary`, `--bs-danger`, `--bs-secondary`, etc. **ne + sont pas modifiées.** L'override cible exclusivement les sélecteurs `.btn-*`. + Conséquence : `dbc.Alert`, badges, `text-danger`, etc. conservent la sémantique + Simplex. +- `info` / `success` / `warning` : utilisés quasi exclusivement par des + `dbc.Alert` (bleu / vert / ambre). On garde la sémantique Simplex pour les + alertes. Aucun bouton `info`/`success`/`warning` n'est restylé. +- `light` (1 bouton, navbar `src/app.py`) et `link` (1 bouton, + `src/saved_views/ui.py`) : laissés tels quels, hors charte des trois rôles. + +## Approche d'implémentation + +**Retenue : CSS-only + audit.** + +Override des classes `.btn-*` dans `style.css`, puis passe de revue des boutons +existants. Minimal, sans churn d'API, respecte le fonctionnement de +`dash-bootstrap-components`. + +**Écartée : helper Python encapsulant `dbc.Button`.** Plus invasif (toucher tous +les appels), aucun bénéfice ici puisque la charte se mappe exactement sur les +`color`/`outline` natifs. + +## Détails CSS à respecter + +- Scoper aux sélecteurs `.btn-*` pour ne pas toucher alertes/badges. +- Gérer explicitement `:hover`, `:focus-visible` (anneau de focus visible — + accessibilité clavier) et `[disabled]`. +- Conserver le `border-radius: 3px` et la typographie (`Inter`, poids 400) déjà en + place pour `.btn-primary`. +- Attention à la spécificité : le `.btn-primary` actuel est ciblé via + `button.btn.btn-primary`. Aligner la spécificité des nouvelles règles pour + éviter qu'elles s'annulent avec celles de `bootstrap.simplex.css`. +- Respecter `prefers-reduced-motion` si des transitions de survol sont ajoutées. + +## Critères de réussite + +- Sur `/compte/vues` : « Renommer » lisible (gris ardoise) et « Supprimer » + clairement rouge, plus aucun mauve ni gris clair illisible. +- Les trois rôles sont visuellement distincts (terracotta / ardoise / rouge) et + cohérents sur toutes les pages. +- Les `dbc.Alert` (info/success/warning) sont inchangées. +- Focus clavier visible sur tous les boutons. diff --git a/src/assets/css/style.css b/src/assets/css/style.css index f5ab93f..34b6e38 100644 --- a/src/assets/css/style.css +++ b/src/assets/css/style.css @@ -39,21 +39,37 @@ h3 { margin: 36px 0 20px 0; } -/* Base Button Styles -button { - font-weight: 400; - background-color: #fff; - border-radius: 3px; - appearance: auto; - border: solid var(--primary-color) 1px; -} */ +/* ========================================================================== + Boutons — charte à 3 rôles (couleur = fonction, remplissage = emphase) + Voir docs/superpowers/specs/2026-06-30-charte-boutons-design.md + Override scopé aux classes .btn-* uniquement (n'affecte pas les alertes, + badges, ni text-danger qui héritent de la sémantique Simplex). + ========================================================================== */ +:root { + --btn-terracotta: #b33821; + --btn-terracotta-text: #fff; + --btn-secondary-text: #344054; + --btn-secondary-border: #5a6570; + --btn-secondary-hover-bg: #f1f3f5; + --btn-danger: #c0392b; + --btn-danger-text: #fff; +} + +/* Base commune : rayon + typo */ +.btn { + border-radius: 3px; + font-family: "Inter", sans-serif; + font-weight: 400; +} + +/* --- PRIMARY plein : action principale validante (1 par contexte) --- */ button.btn.btn-primary, +a.btn.btn-primary, button.show-hide { display: block; - border-radius: 3px; outline: 0; - color: #fff; + color: var(--btn-terracotta-text); border: 0; height: 30px; padding-top: 2px; @@ -65,7 +81,9 @@ button.show-hide { } button.btn.btn-primary:hover, +a.btn.btn-primary:hover, button.show-hide:hover { + color: var(--btn-terracotta-text); background-image: linear-gradient( rgb(239, 126, 103), rgb(209, 86, 63) 26%, @@ -73,9 +91,82 @@ button.show-hide:hover { ); } +/* --- PRIMARY outline : action affirmative de moindre emphase --- */ +.btn.btn-outline-primary { + color: var(--btn-terracotta); + border: 1px solid var(--btn-terracotta); + background-color: transparent; + background-image: none; +} + +.btn.btn-outline-primary:hover, +.btn.btn-outline-primary:focus-visible { + color: var(--btn-terracotta-text); + background-color: var(--btn-terracotta); + border-color: var(--btn-terracotta); +} + +/* --- SECONDARY : action neutre / alternative (gris ardoise) --- */ +.btn.btn-secondary, +.btn.btn-outline-secondary { + color: var(--btn-secondary-text); + border: 1px solid var(--btn-secondary-border); + background-color: transparent; + background-image: none; +} + +.btn.btn-secondary:hover, +.btn.btn-secondary:focus-visible, +.btn.btn-outline-secondary:hover, +.btn.btn-outline-secondary:focus-visible { + color: var(--btn-secondary-text); + background-color: var(--btn-secondary-hover-bg); + border-color: var(--btn-secondary-border); +} + +/* --- DANGER outline : action destructive dans le flux courant --- */ +.btn.btn-outline-danger { + color: var(--btn-danger); + border: 1px solid var(--btn-danger); + background-color: transparent; + background-image: none; +} + +.btn.btn-outline-danger:hover, +.btn.btn-outline-danger:focus-visible { + color: var(--btn-danger-text); + background-color: var(--btn-danger); + border-color: var(--btn-danger); +} + +/* --- DANGER plein : confirmation finale destructive (modale) --- */ +.btn.btn-danger { + color: var(--btn-danger-text); + border: 1px solid var(--btn-danger); + background-color: var(--btn-danger); + background-image: none; +} + +.btn.btn-danger:hover, +.btn.btn-danger:focus-visible { + color: var(--btn-danger-text); + background-color: #a93226; + border-color: #a93226; +} + +/* --- État désactivé commun (conserve l'existant) --- */ +.btn[disabled], button[disabled] { border-color: #ccc; color: #666; + background-image: none; +} + +@media (prefers-reduced-motion: no-preference) { + .btn { + transition: background-color 0.15s ease, color 0.15s ease, + border-color 0.15s ease; + } } /* ========================================================================== diff --git a/src/pages/compte_abonnement.py b/src/pages/compte_abonnement.py index 7ab5ce9..af95155 100644 --- a/src/pages/compte_abonnement.py +++ b/src/pages/compte_abonnement.py @@ -175,7 +175,7 @@ def _active_view(row): "Me désabonner", id="resiliation-trigger", n_clicks=0, - className="btn btn-outline-primary mt-3", + className="btn btn-outline-danger mt-3", ) ) @@ -211,7 +211,7 @@ def _resiliation_modal(end): html.Button( "Je suis sûr", type="submit", - className="btn btn-outline-primary", + className="btn btn-danger", ), ], style={"display": "inline"}, diff --git a/src/pages/compte_admin.py b/src/pages/compte_admin.py index 812d92b..7abf08a 100644 --- a/src/pages/compte_admin.py +++ b/src/pages/compte_admin.py @@ -112,7 +112,7 @@ def _danger_section(): dbc.Button( "Supprimer mon compte", id="delete-open", - color="primary", + color="danger", outline=True, ), dbc.Modal( diff --git a/tests/conftest.py b/tests/conftest.py index 3ff37f7..1244851 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -74,17 +74,17 @@ def test_data(): _cleanup_db_artifacts() -@pytest.fixture(scope="session") -def chrome_options(): +def pytest_setup_options(): + """Options Chrome pour les tests d'intégration Selenium. + + dash.testing n'utilise PAS une fixture `chrome_options` (contrairement à + pytest-selenium) : il appelle ce hook `pytest_setup_options`. C'est donc le + seul point d'entrée qui a réellement un effet. On force `--headless=new` + pour qu'aucune fenêtre Chrome ne s'ouvre pendant les tests. Le dossier de + téléchargement, `--no-sandbox`, `--disable-gpu` et `--disable-dev-shm-usage` + sont déjà gérés par dash.testing.browser. + """ options = Options() - options.add_argument("--window-size=1200,1200 ") - options.add_experimental_option( - "prefs", - { - "download.default_directory": "/home/colin/git/decp.info", - "download.prompt_for_download": False, - "download.directory_upgrade": True, - "safebrowsing.enabled": True, - }, - ) + options.add_argument("--headless=new") + options.add_argument("--window-size=1200,1200") return options diff --git a/tests/test_boutons.py b/tests/test_boutons.py new file mode 100644 index 0000000..52ee5b0 --- /dev/null +++ b/tests/test_boutons.py @@ -0,0 +1,25 @@ +from dash.testing.composite import DashComposite + + +def _rgb_tuple(css_color: str) -> tuple[int, int, int]: + """Parse 'rgb(r, g, b)' ou 'rgba(r, g, b, a)' en (r, g, b).""" + inner = css_color[css_color.index("(") + 1 : css_color.index(")")] + parts = [p.strip() for p in inner.split(",")] + return (int(parts[0]), int(parts[1]), int(parts[2])) + + +def test_btn_primary_is_terracotta(dash_duo: DashComposite): + """Garde de non-régression : le bouton primaire de l'accueil conserve son + dégradé terracotta et son texte blanc après l'override de la charte.""" + from src.app import app + + dash_duo.start_server(app) + dash_duo.wait_for_element("a.btn.btn-primary, button.btn.btn-primary", timeout=10) + btn = dash_duo.find_element("a.btn.btn-primary, button.btn.btn-primary") + + # Le dégradé terracotta est posé via background-image ; le texte est blanc. + bg_image = btn.value_of_css_property("background-image") + color = btn.value_of_css_property("color") + + assert "gradient" in bg_image # dégradé terracotta appliqué + assert _rgb_tuple(color) == (255, 255, 255) # texte blanc