295 lines
8.9 KiB
Markdown
295 lines
8.9 KiB
Markdown
|
|
# Plan d'implémentation v0.801 — UX/UI Polish, Accessibilité & PWA
|
|||
|
|
|
|||
|
|
## État des lieux
|
|||
|
|
|
|||
|
|
| Feature | État | Détail |
|
|||
|
|
|---------|------|--------|
|
|||
|
|
| Thème clair/sombre/auto | ✅ | ThemeProvider, toggle Settings |
|
|||
|
|
| Contraste élevé | ❌ | Non implémenté |
|
|||
|
|
| Compact/confortable | ❌ | Layout unique |
|
|||
|
|
| Accent color | ❌ | Primary fixe |
|
|||
|
|
| Navigation clavier | ⚠️ | Partiel |
|
|||
|
|
| ARIA labels | ⚠️ | Manquants sur boutons icônes |
|
|||
|
|
| Focus visible | ⚠️ | Outline basique |
|
|||
|
|
| Font size ajustable | ❌ | Tailles fixes |
|
|||
|
|
| prefers-reduced-motion | ❌ | Animations non conditionnées |
|
|||
|
|
| PWA installable | ⚠️ | SW basique, manifest incomplet |
|
|||
|
|
| Offline | ❌ | Pas de caching |
|
|||
|
|
| Background playback | ❌ | Audio s'arrête en background |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Fichiers existants clés
|
|||
|
|
|
|||
|
|
- Design tokens : [`index.css`](apps/web/src/index.css) (variables CSS, Tailwind layers)
|
|||
|
|
- ThemeProvider : existant dans `apps/web/src/`
|
|||
|
|
- Settings : [`SettingsAppearance.tsx`](apps/web/src/components/settings/SettingsAppearance.tsx)
|
|||
|
|
- Player hooks : [`apps/web/src/features/player/hooks/`](apps/web/src/features/player/hooks/)
|
|||
|
|
- UI components : [`apps/web/src/components/ui/`](apps/web/src/components/ui/)
|
|||
|
|
- Manifest : [`apps/web/public/manifest.json`](apps/web/public/manifest.json)
|
|||
|
|
- Storybook config : [`apps/web/.storybook/`](apps/web/.storybook/)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Step 1 : Migration + User preferences backend (UX1-04, UX1-05)
|
|||
|
|
|
|||
|
|
**Fichier** : `veza-backend-api/migrations/118_users_preferences.sql` (nouveau)
|
|||
|
|
|
|||
|
|
```sql
|
|||
|
|
ALTER TABLE users ADD COLUMN IF NOT EXISTS preferences JSONB DEFAULT '{}';
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Fichier** : `veza-backend-api/internal/models/user.go` — ajouter :
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
Preferences datatypes.JSON `gorm:"type:jsonb;default:'{}'" json:"preferences,omitempty"`
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Fichier** : `veza-backend-api/internal/handlers/user_handler.go` — ajouter `PUT /users/me/preferences` :
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
func (h *UserHandler) UpdatePreferences(c *gin.Context) {
|
|||
|
|
userID, ok := GetUserIDUUID(c)
|
|||
|
|
if !ok { return }
|
|||
|
|
var prefs map[string]interface{}
|
|||
|
|
if err := c.ShouldBindJSON(&prefs); err != nil {
|
|||
|
|
RespondWithAppError(c, apperrors.NewValidationError("invalid preferences"))
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
// update user.preferences in DB
|
|||
|
|
// ...
|
|||
|
|
RespondSuccess(c, http.StatusOK, gin.H{"preferences": prefs})
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Commit** : `feat(users): add preferences JSONB column and PUT endpoint`
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Step 2 : Design tokens — contraste élevé + compact (UX1-01, UX1-02)
|
|||
|
|
|
|||
|
|
**Fichier** : `apps/web/src/index.css` — ajouter après les thèmes existants :
|
|||
|
|
|
|||
|
|
```css
|
|||
|
|
/* High contrast mode */
|
|||
|
|
[data-contrast="high"] {
|
|||
|
|
--color-foreground: #000000;
|
|||
|
|
--color-background: #ffffff;
|
|||
|
|
--color-muted: #4a4a4a;
|
|||
|
|
--color-muted-foreground: #2a2a2a;
|
|||
|
|
--color-border: #000000;
|
|||
|
|
}
|
|||
|
|
.dark[data-contrast="high"] {
|
|||
|
|
--color-foreground: #ffffff;
|
|||
|
|
--color-background: #000000;
|
|||
|
|
--color-muted: #cccccc;
|
|||
|
|
--color-muted-foreground: #e0e0e0;
|
|||
|
|
--color-border: #ffffff;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* Density modes */
|
|||
|
|
[data-density="compact"] {
|
|||
|
|
font-size: 13px;
|
|||
|
|
}
|
|||
|
|
[data-density="comfortable"] {
|
|||
|
|
font-size: 15px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* Reduced motion */
|
|||
|
|
@media (prefers-reduced-motion: reduce) {
|
|||
|
|
*, *::before, *::after {
|
|||
|
|
animation-duration: 0.01ms !important;
|
|||
|
|
animation-iteration-count: 1 !important;
|
|||
|
|
transition-duration: 0.01ms !important;
|
|||
|
|
scroll-behavior: auto !important;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Commit** : `feat(ui): add high contrast, compact density, reduced motion CSS tokens`
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Step 3 : Settings UI — appearance controls (UX1-01 to UX1-04)
|
|||
|
|
|
|||
|
|
**Fichier** : `apps/web/src/components/settings/SettingsAppearance.tsx` — ajouter :
|
|||
|
|
|
|||
|
|
- Toggle "High Contrast" (data-contrast="high" sur document.documentElement)
|
|||
|
|
- Radio "Compact / Default / Comfortable" (data-density)
|
|||
|
|
- Color picker pour accent hue (CSS variable --color-primary)
|
|||
|
|
- Slider "Font Size" (14px–20px)
|
|||
|
|
- Persistence dans localStorage + sync PUT /users/me/preferences
|
|||
|
|
|
|||
|
|
**Commit** : `feat(settings): add high contrast toggle, density, accent color, font size`
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Step 4 : Accessibilité — ARIA, clavier, focus (UX2-01 to UX2-07)
|
|||
|
|
|
|||
|
|
**Fichier** : `apps/web/src/components/ui/` — pour chaque composant :
|
|||
|
|
|
|||
|
|
- `Button.tsx` : aria-label sur boutons icônes (pas de texte visible)
|
|||
|
|
- `Dialog.tsx` / `Modal.tsx` : aria-modal, focus trap, Escape ferme
|
|||
|
|
- `Input.tsx` : aria-describedby pour messages erreur
|
|||
|
|
- `Tabs.tsx` : role="tablist", role="tab", aria-selected
|
|||
|
|
- `Menu.tsx` / `Dropdown.tsx` : role="menu", Arrow key navigation
|
|||
|
|
- `Table.tsx` : role="table", scope="col" sur headers
|
|||
|
|
- `Pagination.tsx` : aria-label="Pagination", aria-current="page"
|
|||
|
|
|
|||
|
|
**Fichier** : `apps/web/src/index.css` — focus visible amélioré :
|
|||
|
|
|
|||
|
|
```css
|
|||
|
|
:focus-visible {
|
|||
|
|
outline: 2px solid var(--color-primary);
|
|||
|
|
outline-offset: 2px;
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Fichier** : `apps/web/src/components/layout/` — skip-to-content :
|
|||
|
|
|
|||
|
|
```html
|
|||
|
|
<a href="#main-content" className="sr-only focus:not-sr-only ...">Skip to content</a>
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Fichier** : `apps/web/src/hooks/useReducedMotion.ts` (nouveau) :
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
export function useReducedMotion(): boolean {
|
|||
|
|
const [reduced, setReduced] = useState(
|
|||
|
|
() => window.matchMedia('(prefers-reduced-motion: reduce)').matches
|
|||
|
|
);
|
|||
|
|
useEffect(() => {
|
|||
|
|
const mq = window.matchMedia('(prefers-reduced-motion: reduce)');
|
|||
|
|
const handler = (e: MediaQueryListEvent) => setReduced(e.matches);
|
|||
|
|
mq.addEventListener('change', handler);
|
|||
|
|
return () => mq.removeEventListener('change', handler);
|
|||
|
|
}, []);
|
|||
|
|
return reduced;
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Commit** : `feat(a11y): ARIA labels, keyboard nav, focus visible, skip-to-content, reduced motion`
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Step 5 : axe-core Storybook (UX2-08)
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
cd apps/web && npm install --save-dev @storybook/addon-a11y
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Fichier** : `apps/web/.storybook/main.ts` — ajouter `'@storybook/addon-a11y'` dans addons
|
|||
|
|
|
|||
|
|
**Commit** : `feat(storybook): add addon-a11y for automated accessibility testing`
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Step 6 : PWA — manifest, service worker, install prompt (UX3-01 to UX3-03)
|
|||
|
|
|
|||
|
|
**Fichier** : `apps/web/public/manifest.json` — compléter :
|
|||
|
|
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"name": "Veza — Audio Collaborative Platform",
|
|||
|
|
"short_name": "Veza",
|
|||
|
|
"description": "Collaborative audio platform for musicians",
|
|||
|
|
"start_url": "/",
|
|||
|
|
"display": "standalone",
|
|||
|
|
"theme_color": "#0f172a",
|
|||
|
|
"background_color": "#0f172a",
|
|||
|
|
"icons": [
|
|||
|
|
{ "src": "/icons/icon-192.png", "sizes": "192x192", "type": "image/png" },
|
|||
|
|
{ "src": "/icons/icon-512.png", "sizes": "512x512", "type": "image/png" },
|
|||
|
|
{ "src": "/icons/icon-maskable.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" }
|
|||
|
|
],
|
|||
|
|
"shortcuts": [
|
|||
|
|
{ "name": "Dashboard", "url": "/dashboard" },
|
|||
|
|
{ "name": "Marketplace", "url": "/marketplace" },
|
|||
|
|
{ "name": "Go Live", "url": "/live/go-live" }
|
|||
|
|
]
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Fichier** : `apps/web/src/hooks/usePWAInstall.ts` (nouveau)
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
export function usePWAInstall() {
|
|||
|
|
const [prompt, setPrompt] = useState<BeforeInstallPromptEvent | null>(null);
|
|||
|
|
const [isInstalled, setIsInstalled] = useState(false);
|
|||
|
|
|
|||
|
|
useEffect(() => {
|
|||
|
|
const handler = (e: Event) => { e.preventDefault(); setPrompt(e as BeforeInstallPromptEvent); };
|
|||
|
|
window.addEventListener('beforeinstallprompt', handler);
|
|||
|
|
window.addEventListener('appinstalled', () => setIsInstalled(true));
|
|||
|
|
return () => window.removeEventListener('beforeinstallprompt', handler);
|
|||
|
|
}, []);
|
|||
|
|
|
|||
|
|
const install = async () => { if (prompt) { await prompt.prompt(); } };
|
|||
|
|
|
|||
|
|
return { canInstall: !!prompt && !isInstalled, install, isInstalled };
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Commit** : `feat(pwa): complete manifest, install prompt hook, offline caching`
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Step 7 : Background playback mobile (UX3-04)
|
|||
|
|
|
|||
|
|
**Fichier** : `apps/web/src/features/player/hooks/useWakeLock.ts` (nouveau)
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
export function useWakeLock(isPlaying: boolean) {
|
|||
|
|
const lockRef = useRef<WakeLockSentinel | null>(null);
|
|||
|
|
|
|||
|
|
useEffect(() => {
|
|||
|
|
if (!('wakeLock' in navigator)) return;
|
|||
|
|
if (isPlaying) {
|
|||
|
|
navigator.wakeLock.request('screen').then(l => { lockRef.current = l; }).catch(() => {});
|
|||
|
|
} else {
|
|||
|
|
lockRef.current?.release();
|
|||
|
|
lockRef.current = null;
|
|||
|
|
}
|
|||
|
|
return () => { lockRef.current?.release(); };
|
|||
|
|
}, [isPlaying]);
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Commit** : `feat(player): add WakeLock for background playback on mobile`
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Step 8 : Documentation + release
|
|||
|
|
|
|||
|
|
**Fichier** : `CHANGELOG.md` — section v0.801
|
|||
|
|
**Fichier** : `docs/PROJECT_STATE.md` — Dernier tag → v0.801, section v0.801
|
|||
|
|
**Fichier** : `docs/FEATURE_STATUS.md` — section "Livré en v0.801"
|
|||
|
|
|
|||
|
|
**Commit** : `docs: update CHANGELOG, PROJECT_STATE, FEATURE_STATUS for v0.801`
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Step 9 : Rétrospective + archivage + tag
|
|||
|
|
|
|||
|
|
**Fichier** : `docs/RETROSPECTIVE_V0801.md` (nouveau)
|
|||
|
|
**Fichier** : `docs/V0_802_RELEASE_SCOPE.md` (placeholder)
|
|||
|
|
**Fichier** : `docs/SCOPE_CONTROL.md` — mise à jour
|
|||
|
|
**Fichier** : `.cursorrules` — scope v0.802
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
mv docs/V0_801_RELEASE_SCOPE.md docs/archive/
|
|||
|
|
git add . && git commit -m "chore(docs): archive V0_801_RELEASE_SCOPE"
|
|||
|
|
git tag v0.801
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Validation finale
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
cd veza-backend-api && go build ./... && go test ./... -v
|
|||
|
|
cd apps/web && npm run build
|
|||
|
|
# Lighthouse audit: PWA score, a11y score
|
|||
|
|
git tag v0.801
|
|||
|
|
```
|