From b68e7dafd5fb0dd033133fbe1a9310362eea7a5a Mon Sep 17 00:00:00 2001 From: julien rata Date: Wed, 17 Jun 2026 11:12:01 +0200 Subject: [PATCH 1/8] =?UTF-8?q?refactor(front):=20extrait=20SequentialNavC?= =?UTF-8?q?omponent=20partag=C3=A9=20pour=20les=20pages=20de=20d=C3=A9tail?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Les 4 pages de détail (SOLID, Design Patterns, Claude Code, phases de revue) dupliquaient le même bloc de navigation précédent/suivant (template + ~70 lignes de SCSS chacune, identiques au préfixe BEM près). Extraction d'un composant présentationnel `app-sequential-nav` piloté par `prev`/`next`/`routeBase`/ `navLabel`. Comportement, routes et rendu inchangés. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../claude-code-setup-detail.component.html | 34 ++------- .../claude-code-setup-detail.component.scss | 65 ----------------- .../claude-code-setup-detail.component.ts | 31 ++++---- .../design-pattern-detail.component.html | 34 ++------- .../design-pattern-detail.component.scss | 65 ----------------- .../design-pattern-detail.component.ts | 31 ++++---- .../phase-guide/phase-guide.component.html | 34 ++------- .../phase-guide/phase-guide.component.scss | 66 ----------------- .../phase-guide/phase-guide.component.ts | 31 ++++---- .../solid-detail/solid-detail.component.html | 38 +++------- .../solid-detail/solid-detail.component.scss | 65 ----------------- .../solid-detail/solid-detail.component.ts | 31 ++++---- .../sequential-nav.component.html | 28 ++++++++ .../sequential-nav.component.scss | 70 +++++++++++++++++++ .../sequential-nav.component.ts | 37 ++++++++++ 15 files changed, 237 insertions(+), 423 deletions(-) create mode 100644 frontend/src/app/shared/components/sequential-nav/sequential-nav.component.html create mode 100644 frontend/src/app/shared/components/sequential-nav/sequential-nav.component.scss create mode 100644 frontend/src/app/shared/components/sequential-nav/sequential-nav.component.ts diff --git a/frontend/src/app/features/claude-code-setup-detail/claude-code-setup-detail.component.html b/frontend/src/app/features/claude-code-setup-detail/claude-code-setup-detail.component.html index a12fdc5..4327836 100644 --- a/frontend/src/app/features/claude-code-setup-detail/claude-code-setup-detail.component.html +++ b/frontend/src/app/features/claude-code-setup-detail/claude-code-setup-detail.component.html @@ -56,32 +56,10 @@

- + } diff --git a/frontend/src/app/features/claude-code-setup-detail/claude-code-setup-detail.component.scss b/frontend/src/app/features/claude-code-setup-detail/claude-code-setup-detail.component.scss index adb7f93..70c0c39 100644 --- a/frontend/src/app/features/claude-code-setup-detail/claude-code-setup-detail.component.scss +++ b/frontend/src/app/features/claude-code-setup-detail/claude-code-setup-detail.component.scss @@ -106,68 +106,3 @@ 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; - } -} - -.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); -} diff --git a/frontend/src/app/features/claude-code-setup-detail/claude-code-setup-detail.component.ts b/frontend/src/app/features/claude-code-setup-detail/claude-code-setup-detail.component.ts index e7fdbc4..5c11144 100644 --- a/frontend/src/app/features/claude-code-setup-detail/claude-code-setup-detail.component.ts +++ b/frontend/src/app/features/claude-code-setup-detail/claude-code-setup-detail.component.ts @@ -1,6 +1,6 @@ import { Component, computed, inject, signal } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; -import { ActivatedRoute, Router, RouterLink } from '@angular/router'; +import { ActivatedRoute, Router } from '@angular/router'; import { MatCardModule } from '@angular/material/card'; import { MatIconModule } from '@angular/material/icon'; @@ -10,16 +10,14 @@ import { BreadcrumbComponent, BreadcrumbItem, } from '../../shared/components/breadcrumb/breadcrumb.component'; +import { + SequentialNavComponent, + SequentialNavItem, +} from '../../shared/components/sequential-nav/sequential-nav.component'; /** Ordre du parcours — dérivé de l'unique source CLAUDE_CODE_TOPICS. */ const TOPIC_SLUGS = CLAUDE_CODE_TOPICS.map((t) => t.slug); -/** Un sujet voisin pour le parcours séquentiel (précédent/suivant). */ -interface TopicLink { - slug: string; - titre: string; -} - /** * Page de détail d'un sujet de mise en place de Claude Code : définition, * pourquoi, exemples commentés, et comment le mettre en place. @@ -31,7 +29,12 @@ interface TopicLink { */ @Component({ selector: 'app-claude-code-setup-detail', - imports: [RouterLink, BreadcrumbComponent, MatCardModule, MatIconModule], + imports: [ + BreadcrumbComponent, + SequentialNavComponent, + MatCardModule, + MatIconModule, + ], templateUrl: './claude-code-setup-detail.component.html', styleUrl: './claude-code-setup-detail.component.scss', }) @@ -53,8 +56,12 @@ export class ClaudeCodeSetupDetailComponent { ]); /** Sujet précédent / suivant du parcours (undefined aux extrémités). */ - readonly prev = computed(() => this.neighbor(-1)); - readonly next = computed(() => this.neighbor(1)); + readonly prev = computed(() => + this.neighbor(-1, 'précédent') + ); + readonly next = computed(() => + this.neighbor(1, 'suivant') + ); constructor() { this.route.paramMap.pipe(takeUntilDestroyed()).subscribe((params) => { @@ -70,12 +77,12 @@ export class ClaudeCodeSetupDetailComponent { } /** Sujet voisin dans TOPIC_SLUGS (delta -1 = précédent, +1 = suivant). */ - private neighbor(delta: number): TopicLink | undefined { + private neighbor(delta: number, sens: string): SequentialNavItem | undefined { const index = TOPIC_SLUGS.indexOf(this.slug()); if (index === -1) return undefined; const slug = TOPIC_SLUGS[index + delta]; if (!slug) return undefined; const titre = CLAUDE_CODE_TOPICS.find((t) => t.slug === slug)?.titre ?? ''; - return { slug, titre }; + return { slug, label: titre, ariaLabel: `Sujet ${sens} : ${titre}` }; } } diff --git a/frontend/src/app/features/design-pattern-detail/design-pattern-detail.component.html b/frontend/src/app/features/design-pattern-detail/design-pattern-detail.component.html index 23bddd8..7c3f41c 100644 --- a/frontend/src/app/features/design-pattern-detail/design-pattern-detail.component.html +++ b/frontend/src/app/features/design-pattern-detail/design-pattern-detail.component.html @@ -72,32 +72,10 @@

- + } diff --git a/frontend/src/app/features/design-pattern-detail/design-pattern-detail.component.scss b/frontend/src/app/features/design-pattern-detail/design-pattern-detail.component.scss index 02f647d..48ebc79 100644 --- a/frontend/src/app/features/design-pattern-detail/design-pattern-detail.component.scss +++ b/frontend/src/app/features/design-pattern-detail/design-pattern-detail.component.scss @@ -115,68 +115,3 @@ white-space: pre; tab-size: 2; } - -/* Parcours séquentiel précédent / suivant */ -.pattern-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); -} - -.pattern-nav__link { - display: inline-flex; - align-items: center; - gap: var(--cc-space-2); - min-height: 44px; // cible tactile - 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; - } -} - -.pattern-nav__meta { - display: flex; - flex-direction: column; - min-width: 0; -} - -.pattern-nav__dir { - font-size: 0.75rem; - font-weight: 600; - letter-spacing: 0.02em; - text-transform: uppercase; - color: var(--cc-ink-soft); -} - -.pattern-nav__titre { - font-weight: 600; - line-height: var(--cc-lh-tight); -} diff --git a/frontend/src/app/features/design-pattern-detail/design-pattern-detail.component.ts b/frontend/src/app/features/design-pattern-detail/design-pattern-detail.component.ts index b2ce2b2..635fb19 100644 --- a/frontend/src/app/features/design-pattern-detail/design-pattern-detail.component.ts +++ b/frontend/src/app/features/design-pattern-detail/design-pattern-detail.component.ts @@ -1,6 +1,6 @@ import { Component, computed, inject, signal } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; -import { ActivatedRoute, Router, RouterLink } from '@angular/router'; +import { ActivatedRoute, Router } from '@angular/router'; import { MatCardModule } from '@angular/material/card'; import { MatIconModule } from '@angular/material/icon'; @@ -10,16 +10,14 @@ import { BreadcrumbComponent, BreadcrumbItem, } from '../../shared/components/breadcrumb/breadcrumb.component'; +import { + SequentialNavComponent, + SequentialNavItem, +} from '../../shared/components/sequential-nav/sequential-nav.component'; /** Ordre du parcours — dérivé de l'unique source DESIGN_PATTERNS. */ const PATTERN_SLUGS = DESIGN_PATTERNS.map((p) => p.slug); -/** Un patron voisin pour le parcours séquentiel (précédent/suivant). */ -interface PatternLink { - slug: string; - nom: string; -} - /** * Page de détail d'un design pattern : intention, problème, solution, exemple * de code, cas d'usage et pièges. @@ -31,7 +29,12 @@ interface PatternLink { */ @Component({ selector: 'app-design-pattern-detail', - imports: [RouterLink, BreadcrumbComponent, MatCardModule, MatIconModule], + imports: [ + BreadcrumbComponent, + SequentialNavComponent, + MatCardModule, + MatIconModule, + ], templateUrl: './design-pattern-detail.component.html', styleUrl: './design-pattern-detail.component.scss', }) @@ -53,8 +56,12 @@ export class DesignPatternDetailComponent { ]); /** Patron précédent / suivant du parcours (undefined aux extrémités). */ - readonly prev = computed(() => this.neighbor(-1)); - readonly next = computed(() => this.neighbor(1)); + readonly prev = computed(() => + this.neighbor(-1, 'précédent') + ); + readonly next = computed(() => + this.neighbor(1, 'suivant') + ); constructor() { this.route.paramMap.pipe(takeUntilDestroyed()).subscribe((params) => { @@ -70,12 +77,12 @@ export class DesignPatternDetailComponent { } /** Patron voisin dans PATTERN_SLUGS (delta -1 = précédent, +1 = suivant). */ - private neighbor(delta: number): PatternLink | undefined { + private neighbor(delta: number, sens: string): SequentialNavItem | undefined { const index = PATTERN_SLUGS.indexOf(this.slug()); if (index === -1) return undefined; const slug = PATTERN_SLUGS[index + delta]; if (!slug) return undefined; const nom = DESIGN_PATTERNS.find((p) => p.slug === slug)?.nom ?? ''; - return { slug, nom }; + return { slug, label: nom, ariaLabel: `Patron ${sens} : ${nom}` }; } } diff --git a/frontend/src/app/features/phase-guide/phase-guide.component.html b/frontend/src/app/features/phase-guide/phase-guide.component.html index 94c8913..f032dbb 100644 --- a/frontend/src/app/features/phase-guide/phase-guide.component.html +++ b/frontend/src/app/features/phase-guide/phase-guide.component.html @@ -65,32 +65,10 @@

{{ pratique.titre }}

- + } diff --git a/frontend/src/app/features/phase-guide/phase-guide.component.scss b/frontend/src/app/features/phase-guide/phase-guide.component.scss index c0be521..3d9cede 100644 --- a/frontend/src/app/features/phase-guide/phase-guide.component.scss +++ b/frontend/src/app/features/phase-guide/phase-guide.component.scss @@ -172,69 +172,3 @@ } } } - -/* Parcours séquentiel précédent / suivant */ -.phase-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); -} - -.phase-nav__link { - display: inline-flex; - align-items: center; - gap: var(--cc-space-2); - min-height: 44px; // cible tactile - 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); - } - - // Le suivant reste collé à droite même quand le précédent est absent. - &--next { - margin-left: auto; - text-align: right; - } - - mat-icon { - color: var(--cc-primary); - flex-shrink: 0; - } -} - -.phase-nav__meta { - display: flex; - flex-direction: column; - min-width: 0; -} - -.phase-nav__dir { - font-size: 0.75rem; - font-weight: 600; - letter-spacing: 0.02em; - text-transform: uppercase; - color: var(--cc-ink-soft); -} - -.phase-nav__titre { - font-weight: 600; - line-height: var(--cc-lh-tight); -} diff --git a/frontend/src/app/features/phase-guide/phase-guide.component.ts b/frontend/src/app/features/phase-guide/phase-guide.component.ts index 6a5c5cb..11cd962 100644 --- a/frontend/src/app/features/phase-guide/phase-guide.component.ts +++ b/frontend/src/app/features/phase-guide/phase-guide.component.ts @@ -1,6 +1,6 @@ import { Component, computed, inject, signal } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; -import { ActivatedRoute, Router, RouterLink } from '@angular/router'; +import { ActivatedRoute, Router } from '@angular/router'; import { MatCheckboxModule } from '@angular/material/checkbox'; import { MatCardModule } from '@angular/material/card'; import { MatIconModule } from '@angular/material/icon'; @@ -13,6 +13,10 @@ import { BreadcrumbComponent, BreadcrumbItem, } from '../../shared/components/breadcrumb/breadcrumb.component'; +import { + SequentialNavComponent, + SequentialNavItem, +} from '../../shared/components/sequential-nav/sequential-nav.component'; /** Table de correspondance slug de phase → `numero` de section. */ const SLUG_TO_NUMERO: Record = { @@ -30,12 +34,6 @@ const SLUG_TO_NUMERO: Record = { */ const PHASE_SLUGS = Object.keys(SLUG_TO_NUMERO); -/** Une phase voisine pour le parcours séquentiel (précédent/suivant). */ -interface PhaseLink { - slug: string; - titre: string; -} - /** * Page-support d'une phase de revue : lecture (titre + accroche + « pourquoi ») * ET case à cocher par pratique, avec progression sauvegardée par phase. @@ -47,8 +45,8 @@ interface PhaseLink { @Component({ selector: 'app-phase-guide', imports: [ - RouterLink, BreadcrumbComponent, + SequentialNavComponent, MatCheckboxModule, MatCardModule, MatIconModule, @@ -91,8 +89,12 @@ export class PhaseGuideComponent { ]); /** Phase précédente / suivante du parcours (undefined aux extrémités). */ - readonly prev = computed(() => this.neighbor(-1)); - readonly next = computed(() => this.neighbor(1)); + readonly prev = computed(() => + this.neighbor(-1, 'précédente') + ); + readonly next = computed(() => + this.neighbor(1, 'suivante') + ); constructor() { // Réactif au paramètre `:phase` : couvre le chargement initial ET la @@ -133,7 +135,7 @@ export class PhaseGuideComponent { } /** Phase voisine dans PHASE_SLUGS (delta -1 = précédent, +1 = suivant). */ - private neighbor(delta: number): PhaseLink | undefined { + private neighbor(delta: number, sens: string): SequentialNavItem | undefined { const index = PHASE_SLUGS.indexOf(this.slug()); if (index === -1) return undefined; const slug = PHASE_SLUGS[index + delta]; @@ -141,7 +143,7 @@ export class PhaseGuideComponent { const numero = SLUG_TO_NUMERO[slug]; const titre = CODE_REVIEW_SECTIONS.find((s) => s.numero === numero)?.titre ?? ''; - return { slug, titre }; + return { slug, label: titre, ariaLabel: `Phase ${sens} : ${titre}` }; } private resolveSection(slug: string): Section | undefined { @@ -164,6 +166,9 @@ export class PhaseGuideComponent { } private persist(slug: string, ids: Set): void { - localStorage.setItem(this.storageKey(slug), JSON.stringify(Array.from(ids))); + localStorage.setItem( + this.storageKey(slug), + JSON.stringify(Array.from(ids)) + ); } } diff --git a/frontend/src/app/features/solid-detail/solid-detail.component.html b/frontend/src/app/features/solid-detail/solid-detail.component.html index 7200de7..20a65c1 100644 --- a/frontend/src/app/features/solid-detail/solid-detail.component.html +++ b/frontend/src/app/features/solid-detail/solid-detail.component.html @@ -4,7 +4,9 @@
- +

{{ p.nomFr }}

{{ p.nomEn }}

@@ -79,32 +81,10 @@

- + } diff --git a/frontend/src/app/features/solid-detail/solid-detail.component.scss b/frontend/src/app/features/solid-detail/solid-detail.component.scss index 7a5ad66..c87d3f2 100644 --- a/frontend/src/app/features/solid-detail/solid-detail.component.scss +++ b/frontend/src/app/features/solid-detail/solid-detail.component.scss @@ -160,68 +160,3 @@ white-space: pre; tab-size: 2; } - -/* Parcours séquentiel précédent / suivant */ -.principle-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); -} - -.principle-nav__link { - display: inline-flex; - align-items: center; - gap: var(--cc-space-2); - min-height: 44px; // cible tactile - 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; - } -} - -.principle-nav__meta { - display: flex; - flex-direction: column; - min-width: 0; -} - -.principle-nav__dir { - font-size: 0.75rem; - font-weight: 600; - letter-spacing: 0.02em; - text-transform: uppercase; - color: var(--cc-ink-soft); -} - -.principle-nav__titre { - font-weight: 600; - line-height: var(--cc-lh-tight); -} diff --git a/frontend/src/app/features/solid-detail/solid-detail.component.ts b/frontend/src/app/features/solid-detail/solid-detail.component.ts index 647b6c2..061662d 100644 --- a/frontend/src/app/features/solid-detail/solid-detail.component.ts +++ b/frontend/src/app/features/solid-detail/solid-detail.component.ts @@ -1,6 +1,6 @@ import { Component, computed, inject, signal } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; -import { ActivatedRoute, Router, RouterLink } from '@angular/router'; +import { ActivatedRoute, Router } from '@angular/router'; import { MatCardModule } from '@angular/material/card'; import { MatIconModule } from '@angular/material/icon'; @@ -10,16 +10,14 @@ import { BreadcrumbComponent, BreadcrumbItem, } from '../../shared/components/breadcrumb/breadcrumb.component'; +import { + SequentialNavComponent, + SequentialNavItem, +} from '../../shared/components/sequential-nav/sequential-nav.component'; /** Ordre du parcours — dérivé de l'unique source SOLID_PRINCIPLES. */ const PRINCIPLE_SLUGS = SOLID_PRINCIPLES.map((p) => p.slug); -/** Un principe voisin pour le parcours séquentiel (précédent/suivant). */ -interface PrincipleLink { - slug: string; - nomFr: string; -} - /** * Page de détail d'un principe SOLID : définition, pourquoi, exemples de code * à éviter / à préférer, et comment le respecter. @@ -31,7 +29,12 @@ interface PrincipleLink { */ @Component({ selector: 'app-solid-detail', - imports: [RouterLink, BreadcrumbComponent, MatCardModule, MatIconModule], + imports: [ + BreadcrumbComponent, + SequentialNavComponent, + MatCardModule, + MatIconModule, + ], templateUrl: './solid-detail.component.html', styleUrl: './solid-detail.component.scss', }) @@ -53,8 +56,12 @@ export class SolidDetailComponent { ]); /** Principe précédent / suivant du parcours (undefined aux extrémités). */ - readonly prev = computed(() => this.neighbor(-1)); - readonly next = computed(() => this.neighbor(1)); + readonly prev = computed(() => + this.neighbor(-1, 'précédent') + ); + readonly next = computed(() => + this.neighbor(1, 'suivant') + ); constructor() { this.route.paramMap.pipe(takeUntilDestroyed()).subscribe((params) => { @@ -70,12 +77,12 @@ export class SolidDetailComponent { } /** Principe voisin dans PRINCIPLE_SLUGS (delta -1 = précédent, +1 = suivant). */ - private neighbor(delta: number): PrincipleLink | undefined { + private neighbor(delta: number, sens: string): SequentialNavItem | undefined { const index = PRINCIPLE_SLUGS.indexOf(this.slug()); if (index === -1) return undefined; const slug = PRINCIPLE_SLUGS[index + delta]; if (!slug) return undefined; const nomFr = SOLID_PRINCIPLES.find((p) => p.slug === slug)?.nomFr ?? ''; - return { slug, nomFr }; + return { slug, label: nomFr, ariaLabel: `Principe ${sens} : ${nomFr}` }; } } diff --git a/frontend/src/app/shared/components/sequential-nav/sequential-nav.component.html b/frontend/src/app/shared/components/sequential-nav/sequential-nav.component.html new file mode 100644 index 0000000..81c1ed5 --- /dev/null +++ b/frontend/src/app/shared/components/sequential-nav/sequential-nav.component.html @@ -0,0 +1,28 @@ + diff --git a/frontend/src/app/shared/components/sequential-nav/sequential-nav.component.scss b/frontend/src/app/shared/components/sequential-nav/sequential-nav.component.scss new file mode 100644 index 0000000..fd55b43 --- /dev/null +++ b/frontend/src/app/shared/components/sequential-nav/sequential-nav.component.scss @@ -0,0 +1,70 @@ +:host { + display: block; + // Reprend le retrait qui séparait jusqu'ici la nav du contenu de la page. + margin-top: var(--cc-space-7); +} + +/* Parcours séquentiel précédent / suivant */ +.seq-nav { + display: flex; + flex-wrap: wrap; + justify-content: space-between; + gap: var(--cc-space-3); + padding-top: var(--cc-space-4); + border-top: 1px solid var(--cc-border); +} + +.seq-nav__link { + display: inline-flex; + align-items: center; + gap: var(--cc-space-2); + min-height: 44px; // cible tactile + 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); + } + + // Le suivant reste collé à droite même quand le précédent est absent. + &--next { + margin-left: auto; + text-align: right; + } + + mat-icon { + color: var(--cc-primary); + flex-shrink: 0; + } +} + +.seq-nav__meta { + display: flex; + flex-direction: column; + min-width: 0; +} + +.seq-nav__dir { + font-size: 0.75rem; + font-weight: 600; + letter-spacing: 0.02em; + text-transform: uppercase; + color: var(--cc-ink-soft); +} + +.seq-nav__titre { + font-weight: 600; + line-height: var(--cc-lh-tight); +} diff --git a/frontend/src/app/shared/components/sequential-nav/sequential-nav.component.ts b/frontend/src/app/shared/components/sequential-nav/sequential-nav.component.ts new file mode 100644 index 0000000..5c17e86 --- /dev/null +++ b/frontend/src/app/shared/components/sequential-nav/sequential-nav.component.ts @@ -0,0 +1,37 @@ +import { Component, Input } from '@angular/core'; +import { RouterLink } from '@angular/router'; +import { MatIconModule } from '@angular/material/icon'; + +/** Un maillon du parcours séquentiel (précédent ou suivant). */ +export interface SequentialNavItem { + slug: string; + label: string; + /** Libellé d'accessibilité complet (« Principe précédent : … »). */ + ariaLabel: string; +} + +/** + * Navigation « précédent / suivant » d'un parcours séquentiel, partagée par les + * pages de détail (SOLID, Design Patterns, Claude Code, phases de revue). Pilotée + * par `prev`/`next` (undefined aux extrémités) ; `routeBase` est le segment de + * route commun (`/solid`…) auquel le `slug` du maillon est concaténé. + */ +@Component({ + selector: 'app-sequential-nav', + imports: [RouterLink, MatIconModule], + templateUrl: './sequential-nav.component.html', + styleUrl: './sequential-nav.component.scss', +}) +export class SequentialNavComponent { + /** Segment de route commun, ex. `/solid` (concaténé au `slug`). */ + @Input({ required: true }) routeBase!: string; + + /** Libellé d'accessibilité de la balise `