feat: charte graphique des boutons (primary/secondary/danger)
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 <noreply@anthropic.com>
This commit is contained in:
@@ -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) #<issue>
|
||||
|
||||
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
Branche courante : `dev` (auto-déploie sur test.decp.info). Ne pas pousser sans
|
||||
demande explicite.
|
||||
@@ -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.
|
||||
+101
-10
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
|
||||
@@ -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"},
|
||||
|
||||
@@ -112,7 +112,7 @@ def _danger_section():
|
||||
dbc.Button(
|
||||
"Supprimer mon compte",
|
||||
id="delete-open",
|
||||
color="primary",
|
||||
color="danger",
|
||||
outline=True,
|
||||
),
|
||||
dbc.Modal(
|
||||
|
||||
+12
-12
@@ -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
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user