760 lines
25 KiB
Markdown
760 lines
25 KiB
Markdown
# 🎯 Prompt Windsurf — ObsiViewer Nimbus UI (Desktop + Mobile)
|
||
|
||
## ObsiViewer → UI/UX "Nimbus-like" (simple, dense, rapide)
|
||
|
||
**Rôle & mode :** Agis comme **Staff Frontend Engineer Angular 20 + UX designer**. Raisonnement détaillé autorisé. Tu as les pleins pouvoirs de refactor UI, d'ajout de composants, et de migration CSS vers Tailwind. Conserve la compatibilité de toutes features existantes.
|
||
|
||
**Contrainte majeure :** L'interface doit être **100% responsive** (Desktop + Mobile). Un **bouton toggle** dans la navbar permet de basculer entre l'ancienne interface et la nouvelle sans perte d'état.
|
||
|
||
---
|
||
|
||
## Contexte rapide
|
||
|
||
* Projet : **ObsiViewer** (Angular 20 + Tailwind, Node/Express backend).
|
||
* Objectif : Refondre l'interface selon un design **Nimbus Notes**-like.
|
||
* Cœurs d'usage : navigation par **dossiers**, **tags**, **recherche**, **lecture markdown** plein écran, **ToC** à droite, **tri et filtres** rapides.
|
||
* **Nouveauté** : Design adaptatif complet (Desktop/Mobile/Tablet) avec UI toggle persisté.
|
||
|
||
---
|
||
|
||
## 🎯 Objectif final (résumé)
|
||
|
||
### Desktop (≥1024px)
|
||
Refondre l'interface ObsiViewer en **3 colonnes** :
|
||
|
||
1. **Sidebar gauche** (Quick Links, Dossiers arborescents, Tags) — Redimensionnable.
|
||
2. **Colonne centrale** Liste des pages (recherche, filtres dossiers/tags, tris, résultats virtualisés).
|
||
3. **Vue de page** à droite (lecture markdown, barre d'actions, **panneau sommaire/ToC** docké à l'extrême droite).
|
||
|
||
Le tout **compact, performant, thème clair/sombre**, navigation au clavier, états persistés localement.
|
||
|
||
### Mobile/Tablet (<1024px)
|
||
Une navigation **par onglets/drawer** intelligente :
|
||
|
||
- **Tab 1 : Sidebar** (dossiers, tags, recherche) — Panneau full-width ou drawer collapsible.
|
||
- **Tab 2 : Liste** (résultats de recherche) — Full-width scrollable.
|
||
- **Tab 3 : Page** (markdown) — Full-width avec ToC inline collapsible ou drawer.
|
||
|
||
**Gestures** : Swipe horizontal pour navigation onglets, pull-to-refresh, tap = open item.
|
||
|
||
---
|
||
|
||
## 📋 Architecture Feature Flag & Toggle
|
||
|
||
### 1) Toggle UI dans la NavBar
|
||
|
||
Ajouter un **bouton toggle** dans `src/app/layout/app-navbar/app-navbar.component.ts` :
|
||
|
||
```html
|
||
<!-- app-navbar.component.html (snippet) -->
|
||
<div class="flex items-center gap-2">
|
||
<!-- Autres boutons -->
|
||
<button
|
||
(click)="toggleUIMode()"
|
||
[attr.aria-label]="'Toggle ' + (isNimbusMode$ | async ? 'legacy' : 'nimbus') + ' UI'"
|
||
class="p-2 rounded hover:bg-gray-100 dark:hover:bg-gray-800">
|
||
<span *ngIf="(isNimbusMode$ | async)">✨ Nimbus</span>
|
||
<span *ngIf="!(isNimbusMode$ | async)">🔧 Legacy</span>
|
||
</button>
|
||
</div>
|
||
```
|
||
|
||
### 2) Service de gestion du mode UI
|
||
|
||
Créer `src/app/shared/services/ui-mode.service.ts` :
|
||
|
||
```typescript
|
||
import { Injectable, signal } from '@angular/core';
|
||
|
||
@Injectable({ providedIn: 'root' })
|
||
export class UiModeService {
|
||
// Signal pour réactivité fine-grained
|
||
isNimbusMode = signal<boolean>(this.loadUIMode());
|
||
|
||
constructor() {}
|
||
|
||
toggleUIMode() {
|
||
const newMode = !this.isNimbusMode();
|
||
this.isNimbusMode.set(newMode);
|
||
localStorage.setItem('obsiviewer-ui-mode', newMode ? 'nimbus' : 'legacy');
|
||
}
|
||
|
||
private loadUIMode(): boolean {
|
||
if (typeof localStorage === 'undefined') return false;
|
||
const saved = localStorage.getItem('obsiviewer-ui-mode');
|
||
return saved ? saved === 'nimbus' : true; // Nimbus par défaut
|
||
}
|
||
}
|
||
```
|
||
|
||
### 3) Layout wrapper avec feature flag
|
||
|
||
Créer `src/app/layout/app-shell-adaptive/app-shell-adaptive.component.ts` :
|
||
|
||
```typescript
|
||
import { Component, inject } from '@angular/core';
|
||
import { UiModeService } from '@app/shared/services/ui-mode.service';
|
||
import { AppShellNimbusLayoutComponent } from '../app-shell-nimbus/app-shell-nimbus.component';
|
||
import { AppShellLegacyLayoutComponent } from '../app-shell-legacy/app-shell-legacy.component';
|
||
|
||
@Component({
|
||
selector: 'app-shell-adaptive',
|
||
template: `
|
||
@if (uiMode.isNimbusMode()) {
|
||
<app-shell-nimbus-layout></app-shell-nimbus-layout>
|
||
} @else {
|
||
<app-shell-legacy-layout></app-shell-legacy-layout>
|
||
}
|
||
`,
|
||
standalone: true,
|
||
imports: [AppShellNimbusLayoutComponent, AppShellLegacyLayoutComponent],
|
||
})
|
||
export class AppShellAdaptiveComponent {
|
||
uiMode = inject(UiModeService);
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 🎨 Responsive Design Strategy
|
||
|
||
### Breakpoints Tailwind (standard)
|
||
|
||
```typescript
|
||
// tailwind.config.js
|
||
module.exports = {
|
||
theme: {
|
||
screens: {
|
||
'xs': '320px', // iPhone SE
|
||
'sm': '640px', // Petites tablettes
|
||
'md': '768px', // iPad, tablettes
|
||
'lg': '1024px', // Desktop compact
|
||
'xl': '1280px', // Desktop standard
|
||
'2xl': '1536px', // Larges écrans
|
||
},
|
||
},
|
||
};
|
||
```
|
||
|
||
### Mobile First Approach
|
||
|
||
**Développer pour mobile d'abord, puis enrichir pour desktop.**
|
||
|
||
---
|
||
|
||
## 📱 Layouts Responsifs
|
||
|
||
### Desktop Layout (≥1024px)
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────┐
|
||
│ NAVBAR (Dark, fixed, h-14) │
|
||
├────────────────┬──────────────────┬──────────────────────┤
|
||
│ │ │ │
|
||
│ SIDEBAR │ RESULT LIST │ NOTE VIEW + TOC │
|
||
│ (240-440px) │ (virtualized) │ (Resizable) │
|
||
│ Resizable │ │ │
|
||
│ │ │ │
|
||
│ - Quick │ - Search bar │ - Markdown │
|
||
│ Links │ - Filters │ - ToC drawer │
|
||
│ - Folders │ - Items (80px) │ - Actions bar │
|
||
│ - Tags │ - Pagination │ │
|
||
│ │ │ │
|
||
└────────────────┴──────────────────┴──────────────────────┘
|
||
```
|
||
|
||
### Tablet Layout (768px ≤ width < 1024px)
|
||
|
||
```
|
||
┌──────────────────────────────────────┐
|
||
│ NAVBAR + Toggle (fixed, h-14) │
|
||
├──────────────────────────────────────┤
|
||
│ TAB NAVIGATION (fixed, bottom) │
|
||
│ [Sidebar] [List] [Page] [ToC] │
|
||
├──────────────────────────────────────┤
|
||
│ │
|
||
│ ACTIVE TAB CONTENT (scrollable) │
|
||
│ - Drawer si besoin │
|
||
│ - Full-width panels │
|
||
│ │
|
||
└──────────────────────────────────────┘
|
||
```
|
||
|
||
### Mobile Layout (<768px)
|
||
|
||
```
|
||
┌──────────────────────────────────┐
|
||
│ NAVBAR (compact, h-12) │
|
||
│ [Menu] [Search] [Toggle] │
|
||
├──────────────────────────────────┤
|
||
│ │
|
||
│ TAB/DRAWER NAVIGATION │
|
||
│ [≡] [🔍] [📄] [📋] │
|
||
│ │
|
||
│ CONTENT AREA (Full-width) │
|
||
│ - Drawer sidebar (80vw left) │
|
||
│ - Swipeable list (list tab) │
|
||
│ - Markdown full-screen (page) │
|
||
│ - Inline ToC (toggle button) │
|
||
│ │
|
||
├──────────────────────────────────┤
|
||
│ Bottom Navigation (sticky) │
|
||
│ Tab buttons (4 icônes) │
|
||
└──────────────────────────────────┘
|
||
```
|
||
|
||
---
|
||
|
||
## 🎬 Composants Nimbus Responsifs
|
||
|
||
### Desktop/Mobile Variants
|
||
|
||
Chaque composant doit avoir des **variants responsifs** :
|
||
|
||
```
|
||
app-left-sidebar/
|
||
├── app-left-sidebar.component.ts # Logique partagée
|
||
├── app-left-sidebar.desktop.component.ts # ≥1024px (fixed, resizable)
|
||
└── app-left-sidebar.mobile.component.ts # <1024px (drawer)
|
||
|
||
app-center-list/
|
||
├── app-center-list.component.ts
|
||
├── app-center-list.desktop.component.ts # ≥1024px (2 colonnes)
|
||
└── app-center-list.mobile.component.ts # <1024px (full-width)
|
||
|
||
app-note-view/
|
||
├── app-note-view.component.ts
|
||
├── app-note-view.desktop.component.ts # ≥1024px (3 colonnes + ToC)
|
||
└── app-note-view.mobile.component.ts # <1024px (full-width + ToC inline)
|
||
|
||
app-toc-drawer/
|
||
├── app-toc-drawer.component.ts
|
||
├── app-toc-drawer.desktop.component.ts # ≥1024px (Fixed right)
|
||
└── app-toc-drawer.mobile.component.ts # <1024px (Collapsible, inline)
|
||
```
|
||
|
||
### Détection et Injection
|
||
|
||
```typescript
|
||
// app-left-sidebar.component.ts
|
||
import { Component, inject } from '@angular/core';
|
||
import { BreakpointObserver, Breakpoints } from '@angular/cdk/layout';
|
||
|
||
@Component({
|
||
selector: 'app-left-sidebar',
|
||
standalone: true,
|
||
template: `
|
||
@if (isDesktop$ | async) {
|
||
<ng-container *ngComponentOutlet="DesktopSidebarComponent"></ng-container>
|
||
} @else {
|
||
<ng-container *ngComponentOutlet="MobileSidebarDrawerComponent"></ng-container>
|
||
}
|
||
`,
|
||
})
|
||
export class AppLeftSidebarComponent {
|
||
private breakpoint = inject(BreakpointObserver);
|
||
isDesktop$ = this.breakpoint.observe(Breakpoints.Large).pipe(
|
||
map(result => result.matches)
|
||
);
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 📱 Navigation Mobile Avancée
|
||
|
||
### Tab/Drawer Navigation
|
||
|
||
```typescript
|
||
// src/shared/services/mobile-nav.service.ts
|
||
@Injectable({ providedIn: 'root' })
|
||
export class MobileNavService {
|
||
activeTab = signal<'sidebar' | 'list' | 'page' | 'toc'>('list');
|
||
|
||
setTab(tab: 'sidebar' | 'list' | 'page' | 'toc') {
|
||
this.activeTab.set(tab);
|
||
// Persist if needed
|
||
}
|
||
}
|
||
|
||
// Usage in component
|
||
<app-bottom-nav [activeTab]="mobileNav.activeTab()"
|
||
(tabChange)="mobileNav.setTab($event)">
|
||
</app-bottom-nav>
|
||
```
|
||
|
||
### Swipe Navigation (Gestures)
|
||
|
||
```typescript
|
||
// Directive pour détection de swipe
|
||
import { Directive, Output, EventEmitter, HostListener } from '@angular/core';
|
||
|
||
@Directive({
|
||
selector: '[appSwipeNav]',
|
||
standalone: true,
|
||
})
|
||
export class SwipeNavDirective {
|
||
@Output() swipeLeft = new EventEmitter<void>();
|
||
@Output() swipeRight = new EventEmitter<void>();
|
||
|
||
private startX = 0;
|
||
|
||
@HostListener('touchstart', ['$event'])
|
||
onTouchStart(e: TouchEvent) {
|
||
this.startX = e.touches[0].clientX;
|
||
}
|
||
|
||
@HostListener('touchend', ['$event'])
|
||
onTouchEnd(e: TouchEvent) {
|
||
const endX = e.changedTouches[0].clientX;
|
||
const diff = this.startX - endX;
|
||
|
||
if (Math.abs(diff) > 50) { // Seuil minimum
|
||
if (diff > 0) this.swipeLeft.emit();
|
||
else this.swipeRight.emit();
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 🎨 Composants Spécifiques (Mobile-First)
|
||
|
||
### 1) Bottom Navigation (Mobile)
|
||
|
||
```html
|
||
<!-- app-bottom-nav.component.html -->
|
||
<nav class="fixed bottom-0 left-0 right-0 bg-white dark:bg-gray-900 border-t
|
||
border-gray-200 dark:border-gray-800 h-16 flex justify-around md:hidden">
|
||
<button *ngFor="let tab of tabs"
|
||
(click)="selectTab(tab.id)"
|
||
[class.active]="activeTab === tab.id"
|
||
class="flex-1 flex flex-col items-center justify-center gap-1
|
||
text-xs hover:bg-gray-50 dark:hover:bg-gray-800">
|
||
<span class="text-lg">{{ tab.icon }}</span>
|
||
<span>{{ tab.label }}</span>
|
||
</button>
|
||
</nav>
|
||
```
|
||
|
||
### 2) Drawer Sidebar (Mobile)
|
||
|
||
```html
|
||
<!-- app-left-sidebar.mobile.component.html -->
|
||
<aside class="fixed left-0 top-0 bottom-0 w-80vw max-w-xs
|
||
bg-white dark:bg-gray-900 shadow-lg z-50
|
||
transform transition-transform duration-300"
|
||
[class.-translate-x-full]="!isOpen">
|
||
<!-- Contenu sidebar -->
|
||
<button (click)="close()" class="absolute top-4 right-4">✕</button>
|
||
</aside>
|
||
|
||
<!-- Backdrop -->
|
||
<div *ngIf="isOpen"
|
||
(click)="close()"
|
||
class="fixed inset-0 bg-black/50 z-40 md:hidden"></div>
|
||
```
|
||
|
||
### 3) Search Bar Compact (Mobile)
|
||
|
||
```html
|
||
<!-- app-search-bar.mobile.component.html -->
|
||
<div class="sticky top-0 bg-white dark:bg-gray-900 p-2 shadow-sm z-10">
|
||
<div class="flex gap-2">
|
||
<!-- Menu toggle -->
|
||
<button (click)="toggleSidebar()"
|
||
class="p-2 rounded hover:bg-gray-100">☰</button>
|
||
|
||
<!-- Search input (full-width on mobile) -->
|
||
<input type="text"
|
||
placeholder="Search..."
|
||
class="flex-1 px-3 py-2 rounded border dark:border-gray-700">
|
||
|
||
<!-- Filters button (mobile: popover instead of dropdown) -->
|
||
<button (click)="openFilters()"
|
||
class="p-2 rounded hover:bg-gray-100">⚙️</button>
|
||
</div>
|
||
|
||
<!-- Active badges (scrollable horizontally) -->
|
||
<div class="flex gap-1 mt-2 overflow-x-auto">
|
||
<span *ngFor="let badge of activeBadges"
|
||
class="badge badge-sm">
|
||
{{ badge }} ✕
|
||
</span>
|
||
</div>
|
||
</div>
|
||
```
|
||
|
||
### 4) Result List Item (Mobile-Optimized)
|
||
|
||
```html
|
||
<!-- app-result-list-item.component.html -->
|
||
<div class="p-3 border-b hover:bg-gray-50 dark:hover:bg-gray-800 cursor-pointer">
|
||
<!-- Title -->
|
||
<h3 class="font-semibold text-sm sm:text-base truncate">
|
||
{{ item.title }}
|
||
</h3>
|
||
|
||
<!-- Date + Tags (stacked on mobile) -->
|
||
<div class="flex flex-col sm:flex-row sm:items-center gap-1 mt-1 text-xs text-gray-600">
|
||
<span>{{ item.modified | date:'short' }}</span>
|
||
<div class="flex gap-1 flex-wrap">
|
||
<span *ngFor="let tag of item.tags"
|
||
class="badge badge-sm">{{ tag }}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Excerpt (truncated) -->
|
||
<p class="mt-2 text-xs text-gray-600 line-clamp-2">
|
||
{{ item.excerpt }}
|
||
</p>
|
||
</div>
|
||
```
|
||
|
||
### 5) Markdown Viewer (Mobile-Responsive)
|
||
|
||
```html
|
||
<!-- app-markdown-viewer.component.html -->
|
||
<article class="prose dark:prose-invert max-w-none
|
||
prose-sm sm:prose-base
|
||
prose-img:max-w-full prose-img:h-auto
|
||
px-3 sm:px-6 py-4 sm:py-8">
|
||
<!-- Markdown content -->
|
||
<div [innerHTML]="markdownHTML"></div>
|
||
|
||
<!-- ToC (mobile: inline toggle) -->
|
||
<button *ngIf="headings.length > 0"
|
||
(click)="showToC = !showToC"
|
||
class="lg:hidden fixed bottom-20 right-4 p-3 rounded-full bg-blue-500 text-white shadow-lg">
|
||
📋
|
||
</button>
|
||
|
||
<nav *ngIf="showToC" class="lg:hidden fixed inset-0 bg-white dark:bg-gray-900
|
||
z-40 overflow-y-auto p-4">
|
||
<!-- ToC content -->
|
||
</nav>
|
||
</article>
|
||
```
|
||
|
||
### 6) ToC Drawer (Desktop Fixed, Mobile Inline)
|
||
|
||
```typescript
|
||
// app-toc-drawer.component.ts
|
||
@Component({
|
||
selector: 'app-toc-drawer',
|
||
template: `
|
||
<!-- Desktop: Fixed right panel (≥1024px) -->
|
||
<aside class="hidden lg:flex fixed right-0 top-14 bottom-0 w-64
|
||
bg-gray-50 dark:bg-gray-800 border-l
|
||
border-gray-200 dark:border-gray-700
|
||
flex-col overflow-y-auto">
|
||
<app-toc-content [headings]="headings"></app-toc-content>
|
||
</aside>
|
||
|
||
<!-- Mobile: Collapsible inline (< 1024px) -->
|
||
<div *ngIf="(isMobile$ | async)"
|
||
[@slideDown]="showTocMobile ? 'in' : 'out'"
|
||
class="bg-gray-50 dark:bg-gray-800 border-t p-3 max-h-96 overflow-y-auto">
|
||
<app-toc-content [headings]="headings"></app-toc-content>
|
||
</div>
|
||
`,
|
||
animations: [
|
||
trigger('slideDown', [
|
||
state('in', style({ height: '*' })),
|
||
state('out', style({ height: '0px' })),
|
||
transition('in <=> out', animate('200ms ease-in-out')),
|
||
]),
|
||
],
|
||
standalone: true,
|
||
})
|
||
export class AppTocDrawerComponent {
|
||
@Input() headings: Heading[] = [];
|
||
showTocMobile = false;
|
||
isMobile$ = this.breakpoint.observe('(max-width: 1023px)').pipe(
|
||
map(r => r.matches)
|
||
);
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 🎯 Livrables Attendus
|
||
|
||
### Code
|
||
1. **Toggle UI** : `UiModeService`, bouton navbar, `AppShellAdaptiveComponent`
|
||
2. **Responsive Wrappers** : Variants pour chaque composant (Desktop/Mobile)
|
||
3. **Mobile Components** : Bottom nav, drawer sidebar, inline ToC, mobile search
|
||
4. **Gesture Handling** : Swipe navigation directive, touch-friendly interactions
|
||
5. **Breakpoint Utilities** : Service CDK layout, reactive signals
|
||
|
||
### Styling
|
||
1. **Tailwind Config** : Breakpoints personnalisés, tokens tokens clair/sombre
|
||
2. **Mobile-First CSS** : Base mobile, enrichissements desktop via `md:`, `lg:`
|
||
3. **Touch-Friendly** : Boutons ≥44x44px, padding adéquat, hover states
|
||
|
||
### Documentation
|
||
1. **README_UI.md** : Schémas responsive, breakpoints, guide toggle
|
||
2. **MOBILE_GUIDE.md** : Navigation gestures, bottom nav flow, drawer patterns
|
||
3. **RESPONSIVE_CHECKLIST.md** : Tests par breakpoint, checklist A11y mobile
|
||
|
||
### Tests
|
||
1. **E2E** : Toggle persistence, layout switch, gesture navigation
|
||
2. **Visual Regression** : Screenshots desktop/tablet/mobile
|
||
3. **Accessibility** : Touch targets, ARIA labels, keyboard nav (Tab key)
|
||
|
||
---
|
||
|
||
## ⚡ Performance & Mobile Optimizations
|
||
|
||
### Critical Optimizations
|
||
|
||
- **Lazy-load images** : `loading="lazy"`, responsive `srcset`
|
||
- **Virtual scroll** : CDK virtual scroll adapté mobile (item height ≈ 70–80px)
|
||
- **Debounce search** : 300ms sur mobile, 150ms sur desktop
|
||
- **Avoid layout shift** : Aspect ratios, skeleton loaders
|
||
- **Network awareness** : `navigator.connection.effectiveType` pour adapt qualité
|
||
- **Battery saver** : Réduire animations, throttle updates en mode saver
|
||
|
||
### Lighthouse Mobile Targets
|
||
|
||
- Performance ≥ 85
|
||
- Accessibility ≥ 95
|
||
- Best Practices ≥ 90
|
||
|
||
---
|
||
|
||
## 🎮 Raccourcis Clavier & Gestures
|
||
|
||
### Desktop
|
||
- `Ctrl/Cmd + K` : Palette commandes
|
||
- `Ctrl/Cmd + F` : Focus recherche
|
||
- `[` `]` : Replier/ouvrir ToC
|
||
- `Alt + ←/→` : Navigation historique
|
||
|
||
### Mobile/Tablet
|
||
- **Tap** : Ouvrir note/item
|
||
- **Swipe left/right** : Changer onglet (list → page → sidebar)
|
||
- **Long-press** : Menu contextuel (favoris, open in new tab)
|
||
- **Pull-to-refresh** : Rafraîchir liste (optionnel)
|
||
- **Double-tap** : Zoom ToC (mobile)
|
||
|
||
---
|
||
|
||
## 📋 Critères d'Acceptation
|
||
|
||
### Desktop (≥1024px)
|
||
- [x] Layout 3 colonnes (sidebar fixe/resizable, liste, page+ToC)
|
||
- [x] Changement dossier/tag/tri reflété en URL
|
||
- [x] 1000+ items fluide (60fps virtual scroll)
|
||
- [x] ToC synchronisé + repliable
|
||
- [x] Tous les flux clavier-seuls possibles
|
||
|
||
### Tablet (768–1023px)
|
||
- [x] Navigation par onglets (Sidebar / List / Page)
|
||
- [x] Drawer sidebar (80vw, swipeable)
|
||
- [x] Bottom navigation sticky (4 icônes)
|
||
- [x] Contenu full-width par onglet
|
||
- [x] ToC inline collapsible
|
||
|
||
### Mobile (<768px)
|
||
- [x] Drawer sidebar (80vw max)
|
||
- [x] Bottom nav (4 onglets)
|
||
- [x] Search bar compact (menu + search + filters)
|
||
- [x] List items optimisés (titre + date + excerpt)
|
||
- [x] Markdown full-screen
|
||
- [x] ToC overlay ou inline toggle
|
||
- [x] Touch targets ≥ 44x44px
|
||
|
||
### Feature Flag
|
||
- [x] Toggle UI visible dans navbar
|
||
- [x] État persisté (localStorage)
|
||
- [x] Pas de perte d'état lors du switch
|
||
- [x] Legacy UI reste intacte
|
||
|
||
### Accessibility
|
||
- [x] WCAG 2.1 AA sur tous les breakpoints
|
||
- [x] Keyboard navigation complète (Tab, Arrow, Enter)
|
||
- [x] ARIA labels pour navigation tactile
|
||
- [x] Focus visible partout
|
||
- [x] Zoom ≤ 200% sans horizontal scroll
|
||
|
||
### Performance
|
||
- [x] TTI < 2.5s cold start
|
||
- [x] Scroll 60fps sur 1000+ items
|
||
- [x] Lighthouse Mobile ≥ 85 perf, ≥ 95 a11y
|
||
- [x] ImageOptimizations (lazy, srcset, format next-gen)
|
||
|
||
---
|
||
|
||
## 🗂️ Arborescence Fichiers
|
||
|
||
```
|
||
src/app/
|
||
├── layout/
|
||
│ ├── app-shell-adaptive/
|
||
│ │ └── app-shell-adaptive.component.ts # Feature flag wrapper
|
||
│ ├── app-shell-nimbus/
|
||
│ │ ├── app-shell-nimbus.component.ts # 3 colonnes (desktop)
|
||
│ │ ├── app-shell-nimbus.desktop.component.ts
|
||
│ │ └── app-shell-nimbus.mobile.component.ts
|
||
│ └── app-navbar/
|
||
│ ├── app-navbar.component.ts
|
||
│ └── [Bouton toggle UI intégré]
|
||
│
|
||
├── features/
|
||
│ ├── sidebar/
|
||
│ │ ├── app-left-sidebar.component.ts
|
||
│ │ ├── app-left-sidebar.desktop.component.ts
|
||
│ │ └── app-left-sidebar.mobile.component.ts
|
||
│ │
|
||
│ ├── search-bar/
|
||
│ │ ├── app-search-bar.component.ts
|
||
│ │ ├── app-search-bar.desktop.component.ts
|
||
│ │ └── app-search-bar.mobile.component.ts
|
||
│ │
|
||
│ ├── result-list/
|
||
│ │ ├── app-result-list.component.ts
|
||
│ │ ├── app-result-list.desktop.component.ts
|
||
│ │ ├── app-result-list.mobile.component.ts
|
||
│ │ └── app-result-list-item.component.ts
|
||
│ │
|
||
│ ├── note-view/
|
||
│ │ ├── app-note-view.component.ts
|
||
│ │ ├── app-note-view.desktop.component.ts
|
||
│ │ └── app-note-view.mobile.component.ts
|
||
│ │
|
||
│ ├── toc-drawer/
|
||
│ │ ├── app-toc-drawer.component.ts
|
||
│ │ └── app-toc-content.component.ts
|
||
│ │
|
||
│ └── bottom-nav/ [NEW]
|
||
│ ├── app-bottom-nav.component.ts
|
||
│ └── app-bottom-nav.component.html
|
||
│
|
||
├── shared/
|
||
│ ├── services/
|
||
│ │ ├── ui-mode.service.ts # [NEW] Toggle management
|
||
│ │ ├── mobile-nav.service.ts # [NEW] Tab/drawer state
|
||
│ │ └── breakpoint.service.ts # [NEW] Responsive helper
|
||
│ │
|
||
│ ├── directives/
|
||
│ │ └── swipe-nav.directive.ts # [NEW] Gesture detection
|
||
│ │
|
||
│ └── components/
|
||
│ └── resizable-handle/
|
||
│
|
||
└── styles/
|
||
├── tokens.css # Tailwind tokens
|
||
├── responsive.css # Breakpoint utilities
|
||
└── mobile.css # Mobile-specific (touches, etc.)
|
||
```
|
||
|
||
---
|
||
|
||
## 📅 Plan d'Implémentation (ordre conseillé)
|
||
|
||
1. **Feature Flag Infrastructure** (1-2j)
|
||
- `UiModeService` + localStorage persistence
|
||
- `AppShellAdaptiveComponent` wrapper
|
||
- Toggle button dans navbar
|
||
|
||
2. **Responsive Shell & Breakpoints** (2-3j)
|
||
- Desktop layout 3 colonnes (>=1024px)
|
||
- Tailwind breakpoints & tokens
|
||
- Resizable sidebar logic
|
||
|
||
3. **Mobile Navigation & Bottom Nav** (2-3j)
|
||
- `BottomNavComponent` (4 onglets)
|
||
- `MobileNavService` (state management)
|
||
- Tab/drawer routing
|
||
|
||
4. **Mobile Sidebar Drawer** (1-2j)
|
||
- Drawer animé (translate, backdrop)
|
||
- Swipe dismiss directive
|
||
- Z-index management
|
||
|
||
5. **Responsive Components** (3-4j)
|
||
- Search bar variants (desktop/mobile)
|
||
- Result list item responsive
|
||
- Markdown viewer mobile optimizations
|
||
|
||
6. **ToC Drawer Adaptive** (1-2j)
|
||
- Fixed right panel (desktop)
|
||
- Inline toggle (mobile)
|
||
- Animations smooth
|
||
|
||
7. **Gestures & Touch** (1-2j)
|
||
- Swipe nav directive
|
||
- Long-press menu
|
||
- Pull-to-refresh (optionnel)
|
||
|
||
8. **Accessibility & Testing** (2-3j)
|
||
- WCAG 2.1 AA audit
|
||
- Keyboard nav (Tab, Arrow)
|
||
- E2E tests (toggle, breakpoints)
|
||
- Visual regression (3 breakpoints)
|
||
|
||
**Total estimé** : 13–21 jours (équipe 1 FE engineer)
|
||
|
||
---
|
||
|
||
## 🚀 Scripts NPM
|
||
|
||
```bash
|
||
# Dev complet (Nimbus activé par défaut)
|
||
npm run dev
|
||
|
||
# Build production
|
||
npm run build
|
||
|
||
# Tests responsifs (plusieurs breakpoints)
|
||
npm run test:responsive
|
||
|
||
# Lighthouse audit mobile
|
||
npm run audit:lighthouse:mobile
|
||
|
||
# Feature flag (override)
|
||
NIMBUS_UI=false npm run dev # Force legacy UI
|
||
```
|
||
|
||
---
|
||
|
||
## ✅ Checklist Livraison
|
||
|
||
- [ ] Toggle UI visible, fonctionnel, persisté
|
||
- [ ] Desktop (≥1024px) : 3 colonnes, interactions fluides
|
||
- [ ] Tablet (768–1023px) : Onglets + drawer, full-width contenu
|
||
- [ ] Mobile (<768px) : Bottom nav, drawer, touch-friendly
|
||
- [ ] Tous les flux clavier-seuls réalisables
|
||
- [ ] Lighthouse mobile ≥ 85 perf, ≥ 95 a11y
|
||
- [ ] Virtual scroll 60fps sur 1000+ items
|
||
- [ ] Tests E2E (toggle, breakpoints, gestures)
|
||
- [ ] Documentation complète (README_UI.md, MOBILE_GUIDE.md, RESPONSIVE_CHECKLIST.md)
|
||
- [ ] Zéro régression : legacy UI inchangée, Wikilinks, bookmarks, graph intacts
|
||
- [ ] Screenshots before/after 3 breakpoints
|
||
|
||
---
|
||
|
||
## 📖 Documentation à Produire
|
||
|
||
1. **README_UI.md** : Overview, architecture, screenshots 3 breakpoints
|
||
2. **MOBILE_GUIDE.md** : Navigation onglets, gestures, drawer patterns
|
||
3. **RESPONSIVE_CHECKLIST.md** : Tests par breakpoint, device testing
|
||
4. **DEPLOYMENT.md** : Feature flag pour bascule progressive
|
||
5. **ARCHITECTURE_DIAGRAM.md** : Schémas adaptatifs (Mermaid)
|
||
|
||
---
|
||
|
||
## 💡 Notes Importantes
|
||
|
||
- **Mobile First** : Développer pour mobile en premier, puis enrichir desktop.
|
||
- **Persistent State** : Le toggle UI et les filtres actifs doivent persister via localStorage (sans browser storage, utiliser sessionStorage ou service state).
|
||
- **Zero Regression** : L'ancienne interface reste intacte et fonctionnelle.
|
||
- **Performance** : Virtual scroll adapté mobile (40+ items à l'écran), lazy-load images.
|
||
- **Accessibility** : 44x44px touch targets minimum, ARIA labels complets, keyboard nav.
|
||
- **Testing** : Visual regression sur breakpoints clés (375px / 768px / 1440px).
|
||
|
||
---
|
||
|
||
**Exécute maintenant ce plan** : crée les composants, adapte les routes/états, ajoute les styles Tailwind responsifs, branche la recherche et livre le MR conforme aux critères ci-dessus avec toggle UI et compatibilité 100% Desktop/Mobile. |