Skip to content
Merged
62 changes: 62 additions & 0 deletions DECISIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,68 @@ Entrées **antéchronologiques** (la plus récente en haut). La date au format `

---

## 2026-06-17 — Mixins SCSS partagés (`shared/styles/`) pour le CSS dupliqué

- **Contexte** : du SCSS strictement identique était recopié entre features —
blocs des pages de détail (`.code`, socle `.example`, section de contenu,
liste), badge de rôle et carte `.practice` (aperçu des bonnes pratiques ↔
page-support des phases). Refactor à rendu constant, sans toucher au markup
ni aux tokens.
- **Décision** : introduire un premier système de **partiels SCSS de mixins**
sous `frontend/src/app/shared/styles/` (`_detail.scss`, `_badge.scss`,
`_practice-card.scss`), importés par `@use '../../shared/styles/x' as *` dans
chaque composant et appelés via `@include`. Le mixin est inclus **sous le bloc
BEM local** (préfixe propre à la feature conservé) ; les variantes spécifiques
(`--avoid/--prefer`, bord coloré, `--done`) restent locales. Le mixin se
ré-expanse dans le scope de chaque composant → CSS émis et rendu identiques.
- **Options écartées** : (1) des **classes utilitaires globales** dans
`styles.scss` (façon `.cc-stagger`) — rejeté : changerait la portée
(global vs styles scopés par attribut) et imposerait des ajouts de classes
dans les templates ; le mixin garde le scoping et un CSS byte-identique ;
(2) configurer `stylePreprocessorOptions.includePaths` dans `angular.json`
pour raccourcir les imports — rejeté : modifier la config de build pendant un
refactor pur, pour un gain cosmétique sur des chemins relatifs courts
(`../../shared/styles/…`).
- **Point d'attention** : une déclaration placée **après** une règle imbriquée
issue d'un mixin déclenche la dépréciation Sass *mixed-declarations* ; placer
la déclaration **avant** le `@include` (cf. `.example` de design-pattern-detail).
- **Report** : l'item « chrome des hubs » (`.hero`, header, grille — items D-G
de l'audit, partie F) est **différé** : il porte sur les mêmes fichiers qu'une
feature « confettis » en cours (non commitée) et le `.hero` n'y est plus
identique d'un hub à l'autre. À reprendre sur un arbre propre une fois les
confettis intégrés.
- **Trace** : branche `refactor/extract-shared-frontend` — commits `c63baba`
(détail), `141e93c` (badge), `9d954cb` (carte pratique).

## 2026-06-17 — Factorisation des pages de détail (nav séquentielle + utils)

- **Contexte** : les 4 pages de détail (SOLID, Design Patterns, Claude Code,
phases de revue) dupliquaient à l'identique le bloc de navigation
précédent/suivant (template + ~70 lignes de SCSS chacune) et l'arithmétique
de résolution du voisin ; la persistance localStorage d'un `Set` coché était
répétée entre la checklist Code Review et les phases de revue. Refactor à
comportement constant, sans changement d'UI, de route, de slug ni de clé.
- **Décision** : trois extractions ciblées. (A) un composant présentationnel
partagé `shared/components/sequential-nav` (`SequentialNavComponent`, piloté
par `prev`/`next`/`routeBase`/`navLabel`, item `{slug,label,ariaLabel}`) ;
(B) `core/utils/checklist-storage.ts` (`loadCheckedSet`/`persistCheckedSet`,
clé fournie par l'appelant) ; (C) `core/utils/sequential-nav.ts`
(`neighborSlug`). Création du dossier `core/utils/` pour les fonctions pures.
- **Options écartées** : (1) un composant de détail générique unique (rejeté —
les templates de contenu divergent trop, ç'aurait été une abstraction « au cas
où ») ; (2) une classe de base abstraite pour la logique TS commune des détails
(rejeté — `inject()` + signals en classe de base peu idiomatiques ici, gain
faible vs. l'util pur) ; (3) factoriser aussi le « chrome » SCSS des hubs
(`.hero`, header, grilles) et les badges de rôle (différé — partage de SCSS
scopé via global/mixins plus exposé aux régressions visuelles, à traiter
séparément avec vérification de rendu).
- **Pourquoi** : supprimer ~390 lignes dupliquées (nav) + la logique localStorage
et d'index répétée, sans refactor du cœur ni dépendance nouvelle ; le rendu et
l'accessibilité (aria-labels, cible tactile, `:focus-visible`) restent
identiques. `npm run check` (lint + build) vert après chaque extraction.
- **Trace** : branche `refactor/extract-shared-frontend` — commits `b68e7da`
(A), `98ee1df` (B), `f05eb67` (C).

## 2026-06-17 — Pivot de la direction artistique vers une DA festive et animée

- **Contexte** : la DA « atelier / artisan » (papier, cuivre, teal, sobre,
Expand Down
20 changes: 20 additions & 0 deletions frontend/src/app/core/utils/checklist-storage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/**
* Persistance d'un ensemble d'ids cochés dans le localStorage, partagée par les
* outils à cases à cocher (checklist Code Review, phases de revue). La clé est
* fournie par l'appelant (cf. convention `craftcode.<domaine>.<slug>.<quoi>`).
*/

/** Lit l'ensemble d'ids cochés. Tolère un JSON corrompu / un mode privé. */
export function loadCheckedSet(key: string): Set<string> {
try {
const raw = localStorage.getItem(key);
return raw ? new Set<string>(JSON.parse(raw)) : new Set<string>();
} catch {
return new Set<string>();
}
}

/** Persiste l'ensemble d'ids cochés (sérialise le `Set` en tableau). */
export function persistCheckedSet(key: string, ids: Set<string>): void {
localStorage.setItem(key, JSON.stringify(Array.from(ids)));
}
19 changes: 19 additions & 0 deletions frontend/src/app/core/utils/sequential-nav.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/**
* Résolution du slug voisin dans un parcours séquentiel (précédent / suivant),
* partagée par les pages de détail (SOLID, Design Patterns, Claude Code, phases
* de revue). L'ordre du parcours est porté par la liste de slugs fournie.
*/

/**
* Renvoie le slug voisin de `current` dans `slugs` (`delta` -1 = précédent,
* +1 = suivant), ou `undefined` si `current` est inconnu ou aux extrémités.
*/
export function neighborSlug(
slugs: readonly string[],
current: string,
delta: number
): string | undefined {
const index = slugs.indexOf(current);
if (index === -1) return undefined;
return slugs[index + delta];
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
@use '../../shared/styles/badge' as *;
@use '../../shared/styles/practice-card' as *;

:host {
display: block;
max-width: 960px;
Expand Down Expand Up @@ -98,35 +101,7 @@
}

/* Badge de rôle — texte + teinte (jamais la couleur seule) */
.badge {
display: inline-block;
padding: var(--cc-space-1) var(--cc-space-3);
border-radius: var(--cc-radius-pill);
font-size: 0.78rem;
font-weight: 600;
letter-spacing: 0.02em;
white-space: nowrap;

&--equipe {
background: var(--cc-role-equipe-tint);
color: var(--cc-role-equipe);
}

&--auteur {
background: var(--cc-role-auteur-tint);
color: var(--cc-role-auteur);
}

&--relecteur {
background: var(--cc-role-relecteur-tint);
color: var(--cc-role-relecteur);
}

&--mixte {
background: var(--cc-role-mixte-tint);
color: var(--cc-role-mixte);
}
}
@include role-badge;

/* Lien vers la page-support */
.support-link {
Expand All @@ -143,63 +118,5 @@
}

.practice {
border-radius: var(--cc-radius-md);
transition:
transform var(--cc-transition-bounce),
box-shadow var(--cc-transition);

&:hover,
&:focus-within {
transform: translateY(-6px) scale(1.015);
box-shadow: var(--cc-shadow-2);
}

&__top {
display: flex;
align-items: flex-start;
gap: var(--cc-space-2);
margin-bottom: var(--cc-space-2);
}

&__icon {
color: var(--cc-primary);
flex-shrink: 0;
}

&__title {
margin: 0;
font-size: var(--cc-fs-h3);
font-weight: 600;
color: var(--cc-ink);
line-height: var(--cc-lh-tight);
}

&__accroche {
margin: 0 0 var(--cc-space-3);
color: var(--cc-ink-soft);
font-size: 0.9rem;
line-height: 1.5;
}

&__why {
display: flex;
align-items: flex-start;
gap: var(--cc-space-2);
margin: 0;
padding-top: var(--cc-space-3);
border-top: 1px solid var(--cc-border);
color: var(--cc-ink-soft);
font-size: var(--cc-fs-small);
line-height: 1.45;
font-style: italic;
}

&__why-icon {
font-size: 1.05rem;
width: 1.05rem;
height: 1.05rem;
color: var(--cc-accent);
flex-shrink: 0;
margin-top: 0.05rem;
}
@include practice-card;
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,32 +56,10 @@ <h2 class="ccd-block__title">
</section>

<!-- Parcours séquentiel : sujet précédent / suivant -->
<nav class="topic-nav" aria-label="Navigation entre les sujets">
@if (prev(); as pr) {
<a
class="topic-nav__link topic-nav__link--prev"
[routerLink]="['/claude-code-setup', pr.slug]"
[attr.aria-label]="'Sujet précédent : ' + pr.titre"
>
<mat-icon aria-hidden="true">arrow_back</mat-icon>
<span class="topic-nav__meta">
<span class="topic-nav__dir">Précédent</span>
<span class="topic-nav__titre">{{ pr.titre }}</span>
</span>
</a>
}
@if (next(); as nx) {
<a
class="topic-nav__link topic-nav__link--next"
[routerLink]="['/claude-code-setup', nx.slug]"
[attr.aria-label]="'Sujet suivant : ' + nx.titre"
>
<span class="topic-nav__meta">
<span class="topic-nav__dir">Suivant</span>
<span class="topic-nav__titre">{{ nx.titre }}</span>
</span>
<mat-icon aria-hidden="true">arrow_forward</mat-icon>
</a>
}
</nav>
<app-sequential-nav
routeBase="/claude-code-setup"
navLabel="Navigation entre les sujets"
[prev]="prev()"
[next]="next()"
/>
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
@use '../../shared/styles/detail' as *;

:host {
display: block;
max-width: 960px;
Expand Down Expand Up @@ -45,33 +47,11 @@
}

.ccd-block {
margin-bottom: var(--cc-space-6);

&__title {
display: flex;
align-items: center;
gap: var(--cc-space-2);
margin: 0 0 var(--cc-space-3);
font-size: clamp(1.2rem, 2.8vw, 1.4rem);
font-weight: 600;
color: var(--cc-ink);

mat-icon {
color: var(--cc-accent);
}
}
@include detail-block;
}

.ccd-list {
margin: 0;
padding-left: var(--cc-space-5);
max-width: var(--cc-measure);
color: var(--cc-ink-soft);
line-height: var(--cc-lh-body);

li {
margin-bottom: var(--cc-space-2);
}
@include detail-list;
}

/* Exemples concrets avec blocs de code */
Expand All @@ -81,93 +61,7 @@
}

.example {
border-radius: var(--cc-radius-md);

&__legende {
margin: 0 0 var(--cc-space-3);
color: var(--cc-ink-soft);
font-size: var(--cc-fs-small);
font-style: italic;
}
}

.code {
margin: 0;
padding: var(--cc-space-4);
background: var(--cc-surface-2);
border: 1px solid var(--cc-border);
border-radius: var(--cc-radius-sm);
color: var(--cc-ink);
font-family:
'SFMono-Regular', 'Cascadia Code', Consolas, 'Courier New', monospace;
font-size: 0.82rem;
line-height: 1.55;
overflow-x: auto;
white-space: pre;
tab-size: 2;
}

/* Parcours séquentiel précédent / suivant */
.topic-nav {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
gap: var(--cc-space-3);
margin-top: var(--cc-space-7);
padding-top: var(--cc-space-4);
border-top: 1px solid var(--cc-border);
}

.topic-nav__link {
display: inline-flex;
align-items: center;
gap: var(--cc-space-2);
min-height: 44px;
max-width: 100%;
padding: var(--cc-space-2) var(--cc-space-4);
border: 1px solid var(--cc-border);
border-radius: var(--cc-radius-md);
background: var(--cc-surface);
color: var(--cc-ink);
text-decoration: none;
transition:
border-color var(--cc-transition),
box-shadow var(--cc-transition),
transform var(--cc-transition-bounce);

&:hover,
&:focus-visible {
border-color: var(--cc-primary);
box-shadow: var(--cc-shadow-2);
transform: translateY(-3px);
}

&--next {
margin-left: auto;
text-align: right;
}

mat-icon {
color: var(--cc-primary);
flex-shrink: 0;
}
@include detail-example;
}

.topic-nav__meta {
display: flex;
flex-direction: column;
min-width: 0;
}

.topic-nav__dir {
font-size: 0.75rem;
font-weight: 600;
letter-spacing: 0.02em;
text-transform: uppercase;
color: var(--cc-ink-soft);
}

.topic-nav__titre {
font-weight: 600;
line-height: var(--cc-lh-tight);
}
@include detail-code;
Loading
Loading