25 KiB
🎯 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 :
- Sidebar gauche (Quick Links, Dossiers arborescents, Tags) — Redimensionnable.
- Colonne centrale Liste des pages (recherche, filtres dossiers/tags, tris, résultats virtualisés).
- 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 :
<!-- 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 :
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 :
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)
// 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
// 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
// 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)
// 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)
<!-- 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)
<!-- 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)
<!-- 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)
<!-- 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)
<!-- 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)
// 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
- Toggle UI :
UiModeService, bouton navbar,AppShellAdaptiveComponent - Responsive Wrappers : Variants pour chaque composant (Desktop/Mobile)
- Mobile Components : Bottom nav, drawer sidebar, inline ToC, mobile search
- Gesture Handling : Swipe navigation directive, touch-friendly interactions
- Breakpoint Utilities : Service CDK layout, reactive signals
Styling
- Tailwind Config : Breakpoints personnalisés, tokens tokens clair/sombre
- Mobile-First CSS : Base mobile, enrichissements desktop via
md:,lg: - Touch-Friendly : Boutons ≥44x44px, padding adéquat, hover states
Documentation
- README_UI.md : Schémas responsive, breakpoints, guide toggle
- MOBILE_GUIDE.md : Navigation gestures, bottom nav flow, drawer patterns
- RESPONSIVE_CHECKLIST.md : Tests par breakpoint, checklist A11y mobile
Tests
- E2E : Toggle persistence, layout switch, gesture navigation
- Visual Regression : Screenshots desktop/tablet/mobile
- Accessibility : Touch targets, ARIA labels, keyboard nav (Tab key)
⚡ Performance & Mobile Optimizations
Critical Optimizations
- Lazy-load images :
loading="lazy", responsivesrcset - 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.effectiveTypepour 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 commandesCtrl/Cmd + F: Focus recherche[]: Replier/ouvrir ToCAlt + ←/→: 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)
- Layout 3 colonnes (sidebar fixe/resizable, liste, page+ToC)
- Changement dossier/tag/tri reflété en URL
- 1000+ items fluide (60fps virtual scroll)
- ToC synchronisé + repliable
- Tous les flux clavier-seuls possibles
Tablet (768–1023px)
- Navigation par onglets (Sidebar / List / Page)
- Drawer sidebar (80vw, swipeable)
- Bottom navigation sticky (4 icônes)
- Contenu full-width par onglet
- ToC inline collapsible
Mobile (<768px)
- Drawer sidebar (80vw max)
- Bottom nav (4 onglets)
- Search bar compact (menu + search + filters)
- List items optimisés (titre + date + excerpt)
- Markdown full-screen
- ToC overlay ou inline toggle
- Touch targets ≥ 44x44px
Feature Flag
- Toggle UI visible dans navbar
- État persisté (localStorage)
- Pas de perte d'état lors du switch
- Legacy UI reste intacte
Accessibility
- WCAG 2.1 AA sur tous les breakpoints
- Keyboard navigation complète (Tab, Arrow, Enter)
- ARIA labels pour navigation tactile
- Focus visible partout
- Zoom ≤ 200% sans horizontal scroll
Performance
- TTI < 2.5s cold start
- Scroll 60fps sur 1000+ items
- Lighthouse Mobile ≥ 85 perf, ≥ 95 a11y
- 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é)
-
Feature Flag Infrastructure (1-2j)
UiModeService+ localStorage persistenceAppShellAdaptiveComponentwrapper- Toggle button dans navbar
-
Responsive Shell & Breakpoints (2-3j)
- Desktop layout 3 colonnes (>=1024px)
- Tailwind breakpoints & tokens
- Resizable sidebar logic
-
Mobile Navigation & Bottom Nav (2-3j)
BottomNavComponent(4 onglets)MobileNavService(state management)- Tab/drawer routing
-
Mobile Sidebar Drawer (1-2j)
- Drawer animé (translate, backdrop)
- Swipe dismiss directive
- Z-index management
-
Responsive Components (3-4j)
- Search bar variants (desktop/mobile)
- Result list item responsive
- Markdown viewer mobile optimizations
-
ToC Drawer Adaptive (1-2j)
- Fixed right panel (desktop)
- Inline toggle (mobile)
- Animations smooth
-
Gestures & Touch (1-2j)
- Swipe nav directive
- Long-press menu
- Pull-to-refresh (optionnel)
-
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
# 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
- README_UI.md : Overview, architecture, screenshots 3 breakpoints
- MOBILE_GUIDE.md : Navigation onglets, gestures, drawer patterns
- RESPONSIVE_CHECKLIST.md : Tests par breakpoint, device testing
- DEPLOYMENT.md : Feature flag pour bascule progressive
- 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.