801e6a050b
- Documentation archivée et réorganisée - Backend: Ajout tests, migrations, library service, rate limiting - Frontend: Suppression Flutter, focus sur interface web HTML/JS - Tailwind CSS ajouté pour le style - Améliorations UX et corrections bugs Generated with [Claude Code](https://claude.com/claude-code) via [Happy](https://happy.engineering) Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: Happy <yesreply@happy.engineering>
691 lines
20 KiB
Markdown
691 lines
20 KiB
Markdown
# Guide d'Implémentation du Design System - AudiOhm
|
|
|
|
Ce guide vous explique comment appliquer le nouveau système de design moderne à votre application Flutter AudiOhm.
|
|
|
|
## 📋 Sommaire
|
|
|
|
1. [Vue d'ensemble](#vue-densemble)
|
|
2. [Structure du design system](#structure-du-design-system)
|
|
3. [Implémentation dans Flutter](#implémentation-dans-flutter)
|
|
4. [Migration du code existant](#migration-du-code-existant)
|
|
5. [Checklist de validation](#checklist-de-validation)
|
|
|
|
---
|
|
|
|
## Vue d'ensemble
|
|
|
|
### Objectifs
|
|
|
|
✅ Moderniser l'UI/UX selon les standards 2025
|
|
✅ Préserver l'identité cyberpunk néon
|
|
✅ Améliorer l'accessibilité (WCAG AA)
|
|
✅ Optimiser les performances
|
|
✅ Standardiser les composants
|
|
|
|
### Changements Majeurs
|
|
|
|
| Aspect | Avant | Après |
|
|
|--------|-------|-------|
|
|
| **Contraste** | Parfois faible | Minimum 4.5:1 (WCAG AA) |
|
|
| **Icônes** | Mixtes | SVG unifiés (Lucide) |
|
|
| **Transitions** | Instables ou 0ms | 150-300ms standardisées |
|
|
| **Spacing** | Incohérent | Système de 4px |
|
|
| **Typography** | Outfit uniquement | Space Grotesk + Outfit |
|
|
| **Couleurs** | Néon sans structure | Palette sémantique claire |
|
|
|
|
---
|
|
|
|
## Structure du Design System
|
|
|
|
### Fichiers Créés
|
|
|
|
```
|
|
design-system/
|
|
├── MASTER.md # Règles globales (source de vérité)
|
|
└── pages/
|
|
├── home.md # Override pour page d'accueil
|
|
├── search.md # Override pour page de recherche
|
|
└── player.md # Override pour page lecteur
|
|
```
|
|
|
|
### Comment Utiliser
|
|
|
|
Pour chaque page/component que vous créez ou modifiez :
|
|
|
|
1. **Consultez d'abord le MASTER.md** pour les règles de base
|
|
2. **Vérifiez s'il existe un override** pour la page spécifique
|
|
3. **Si un override existe**, ses règles priment sur le MASTER
|
|
4. **Sinon**, appliquez les règles du MASTER
|
|
|
|
**Exemple :**
|
|
```
|
|
"Je crée la page Player"
|
|
→ Lire design-system/MASTER.md
|
|
→ Lire design-system/pages/player.md
|
|
→ Les règles de player.md priment sur MASTER.md
|
|
```
|
|
|
|
---
|
|
|
|
## Implémentation dans Flutter
|
|
|
|
### 1. Créer le fichier de couleurs
|
|
|
|
Créez `lib/core/theme/colors.dart`:
|
|
|
|
```dart
|
|
import 'package:flutter/material.dart';
|
|
|
|
class AppColors {
|
|
// Background Colors
|
|
static const Color background = Color(0xFF0A0E27);
|
|
static const Color surface = Color(0xFF151932);
|
|
static const Color surfaceElevated = Color(0xFF1F2342);
|
|
static const Color border = Color(0xFF2A2F4A);
|
|
|
|
// Neon Accents
|
|
static const Color primary = Color(0xFF00F0FF);
|
|
static const Color secondary = Color(0xFFBF00FF);
|
|
static const Color accent = Color(0xFFFF006E);
|
|
static const Color success = Color(0xFF00FF94);
|
|
static const Color warning = Color(0xFFFFB800);
|
|
static const Color error = Color(0xFFFF3B3B);
|
|
|
|
// Text Colors
|
|
static const Color textPrimary = Color(0xFFF0F4F8);
|
|
static const Color textSecondary = Color(0xFF9BA3B8);
|
|
static const Color textTertiary = Color(0xFF6B7280);
|
|
static const Color textInverted = Color(0xFF0A0E27);
|
|
|
|
// Gradients
|
|
static const LinearGradient primaryGradient = LinearGradient(
|
|
begin: Alignment.topLeft,
|
|
end: Alignment.bottomRight,
|
|
colors: [primary, Color(0xFF00C8FF)],
|
|
);
|
|
|
|
static const LinearGradient secondaryGradient = LinearGradient(
|
|
begin: Alignment.topLeft,
|
|
end: Alignment.bottomRight,
|
|
colors: [accent, error],
|
|
);
|
|
}
|
|
```
|
|
|
|
### 2. Créer le fichier de typography
|
|
|
|
Créez `lib/core/theme/typography.dart`:
|
|
|
|
```dart
|
|
import 'package:flutter/material.dart';
|
|
import 'package:google_fonts/google_fonts.dart';
|
|
|
|
class AppTypography {
|
|
// Heading Font - Space Grotesk
|
|
static TextStyle get heading {
|
|
return GoogleFonts.spaceGrotesk(
|
|
fontWeight: FontWeight.w700,
|
|
color: AppColors.textPrimary,
|
|
);
|
|
}
|
|
|
|
// Body Font - Outfit
|
|
static TextStyle get body {
|
|
return GoogleFonts.outfit(
|
|
fontWeight: FontWeight.w400,
|
|
color: AppColors.textPrimary,
|
|
);
|
|
}
|
|
|
|
// Type Scale
|
|
static const double displaySize = 48.0;
|
|
static const double h1Size = 36.0;
|
|
static const double h2Size = 28.0;
|
|
static const double h3Size = 22.0;
|
|
static const double bodyLargeSize = 18.0;
|
|
static const double bodySize = 16.0;
|
|
static const double bodySmallSize = 14.0;
|
|
static const double captionSize = 12.0;
|
|
static const double overlineSize = 11.0;
|
|
|
|
// Text Styles
|
|
static TextStyle get display => heading.copyWith(fontSize: displaySize);
|
|
|
|
static TextStyle get h1 => heading.copyWith(fontSize: h1Size);
|
|
|
|
static TextStyle get h2 => heading.copyWith(
|
|
fontSize: h2Size,
|
|
fontWeight: FontWeight.w600,
|
|
);
|
|
|
|
static TextStyle get h3 => heading.copyWith(
|
|
fontSize: h3Size,
|
|
fontWeight: FontWeight.w600,
|
|
);
|
|
|
|
static TextStyle get bodyLarge => body.copyWith(
|
|
fontSize: bodyLargeSize,
|
|
height: 1.5,
|
|
);
|
|
|
|
static TextStyle get bodyText => body.copyWith(
|
|
fontSize: bodySize,
|
|
height: 1.6,
|
|
);
|
|
|
|
static TextStyle get bodySmall => body.copyWith(
|
|
fontSize: bodySmallSize,
|
|
height: 1.6,
|
|
color: AppColors.textSecondary,
|
|
);
|
|
|
|
static TextStyle get caption => body.copyWith(
|
|
fontSize: captionSize,
|
|
fontWeight: FontWeight.w500,
|
|
height: 1.5,
|
|
color: AppColors.textSecondary,
|
|
);
|
|
|
|
static TextStyle get overline => body.copyWith(
|
|
fontSize: overlineSize,
|
|
fontWeight: FontWeight.w600,
|
|
height: 1.4,
|
|
color: AppColors.textPrimary,
|
|
letterSpacing: 0.5,
|
|
);
|
|
}
|
|
```
|
|
|
|
### 3. Créer le thème MaterialApp
|
|
|
|
Créez `lib/core/theme/app_theme.dart`:
|
|
|
|
```dart
|
|
import 'package:flutter/material.dart';
|
|
import 'colors.dart';
|
|
import 'typography.dart';
|
|
|
|
class AppTheme {
|
|
static ThemeData get darkTheme {
|
|
return ThemeData(
|
|
useMaterial3: true,
|
|
brightness: Brightness.dark,
|
|
|
|
// Color Scheme
|
|
colorScheme: const ColorScheme.dark(
|
|
primary: AppColors.primary,
|
|
secondary: AppColors.secondary,
|
|
surface: AppColors.surface,
|
|
error: AppColors.error,
|
|
onPrimary: AppColors.textInverted,
|
|
onSecondary: AppColors.textPrimary,
|
|
onSurface: AppColors.textPrimary,
|
|
onError: AppColors.textPrimary,
|
|
),
|
|
|
|
// Scaffold
|
|
scaffoldBackgroundColor: AppColors.background,
|
|
|
|
// Typography
|
|
fontFamily: 'Outfit',
|
|
textTheme: TextTheme(
|
|
displayLarge: AppTypography.display,
|
|
headlineMedium: AppTypography.h1,
|
|
headlineSmall: AppTypography.h2,
|
|
titleLarge: AppTypography.h3,
|
|
bodyLarge: AppTypography.bodyLarge,
|
|
bodyMedium: AppTypography.bodyText,
|
|
bodySmall: AppTypography.bodySmall,
|
|
labelSmall: AppTypography.caption,
|
|
),
|
|
|
|
// Card Theme
|
|
cardTheme: CardTheme(
|
|
color: AppColors.surface,
|
|
elevation: 0,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(16),
|
|
),
|
|
side: BorderSide(color: AppColors.border, width: 1),
|
|
),
|
|
|
|
// Input Decoration
|
|
inputDecorationTheme: InputDecorationTheme(
|
|
filled: true,
|
|
fillColor: AppColors.background,
|
|
border: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(8),
|
|
borderSide: BorderSide(color: AppColors.border),
|
|
),
|
|
enabledBorder: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(8),
|
|
borderSide: BorderSide(color: AppColors.border),
|
|
),
|
|
focusedBorder: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(8),
|
|
borderSide: BorderSide(color: AppColors.primary),
|
|
),
|
|
focusColor: AppColors.primary,
|
|
contentPadding: const EdgeInsets.symmetric(
|
|
horizontal: 16,
|
|
vertical: 12,
|
|
),
|
|
),
|
|
|
|
// Elevated Button Theme
|
|
elevatedButtonTheme: ElevatedButtonThemeData(
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: AppColors.primary,
|
|
foregroundColor: AppColors.textInverted,
|
|
elevation: 0,
|
|
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
textStyle: AppTypography.bodyText.copyWith(
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
).copyWith(
|
|
elevation: MaterialStateProperty.resolveWith<double>((states) {
|
|
if (states.contains(MaterialState.pressed)) return 0;
|
|
if (states.contains(MaterialState.hovered)) return 4;
|
|
return 0;
|
|
}),
|
|
),
|
|
),
|
|
|
|
// Outline Button Theme
|
|
outlinedButtonTheme: OutlinedButtonThemeData(
|
|
style: OutlinedButton.styleFrom(
|
|
foregroundColor: AppColors.primary,
|
|
side: BorderSide(color: AppColors.primary),
|
|
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
textStyle: AppTypography.bodyText.copyWith(
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
),
|
|
|
|
// Icon Theme
|
|
iconTheme: const IconThemeData(
|
|
color: AppColors.textSecondary,
|
|
size: 24,
|
|
),
|
|
|
|
// Divider
|
|
dividerTheme: const DividerThemeData(
|
|
color: AppColors.border,
|
|
thickness: 1,
|
|
space: 1,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
```
|
|
|
|
### 4. Créer des composants réutilisables
|
|
|
|
#### Bouton Primaire avec Glow
|
|
|
|
Créez `lib/widgets/buttons/primary_button.dart`:
|
|
|
|
```dart
|
|
import 'package:flutter/material.dart';
|
|
import '../core/theme/colors.dart';
|
|
|
|
class PrimaryButton extends StatelessWidget {
|
|
final String text;
|
|
final VoidCallback? onPressed;
|
|
final bool isLoading;
|
|
final double? width;
|
|
|
|
const PrimaryButton({
|
|
Key? key,
|
|
required this.text,
|
|
this.onPressed,
|
|
this.isLoading = false,
|
|
this.width,
|
|
}) : super(key: key);
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return AnimatedContainer(
|
|
duration: const Duration(milliseconds: 200),
|
|
width: width,
|
|
decoration: BoxDecoration(
|
|
gradient: AppColors.primaryGradient,
|
|
borderRadius: BorderRadius.circular(8),
|
|
boxShadow: onPressed != null
|
|
? [
|
|
BoxShadow(
|
|
color: AppColors.primary.withOpacity(0.3),
|
|
blurRadius: 20,
|
|
offset: const Offset(0, 4),
|
|
),
|
|
]
|
|
: null,
|
|
),
|
|
child: Material(
|
|
color: Colors.transparent,
|
|
child: InkWell(
|
|
onTap: isLoading ? null : onPressed,
|
|
borderRadius: BorderRadius.circular(8),
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
|
child: Center(
|
|
child: isLoading
|
|
? const SizedBox(
|
|
width: 20,
|
|
height: 20,
|
|
child: CircularProgressIndicator(
|
|
strokeWidth: 2,
|
|
valueColor: AlwaysStoppedAnimation(AppColors.textInverted),
|
|
),
|
|
)
|
|
: Text(
|
|
text,
|
|
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
|
fontWeight: FontWeight.w600,
|
|
color: AppColors.textInverted,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
```
|
|
|
|
#### Carte Album avec Hover
|
|
|
|
Créez `lib/widgets/cards/album_card.dart`:
|
|
|
|
```dart
|
|
import 'package:flutter/material.dart';
|
|
import '../../core/theme/colors.dart';
|
|
import '../../core/theme/typography.dart';
|
|
|
|
class AlbumCard extends StatefulWidget {
|
|
final String imageUrl;
|
|
final String title;
|
|
final String subtitle;
|
|
final VoidCallback? onTap;
|
|
|
|
const AlbumCard({
|
|
Key? key,
|
|
required this.imageUrl,
|
|
required this.title,
|
|
required this.subtitle,
|
|
this.onTap,
|
|
}) : super(key: key);
|
|
|
|
@override
|
|
State<AlbumCard> createState() => _AlbumCardState();
|
|
}
|
|
|
|
class _AlbumCardState extends State<AlbumCard>
|
|
with SingleTickerProviderStateMixin {
|
|
late AnimationController _animationController;
|
|
late Animation<double> _scaleAnimation;
|
|
bool _isHovered = false;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_animationController = AnimationController(
|
|
duration: const Duration(milliseconds: 200),
|
|
vsync: this,
|
|
);
|
|
_scaleAnimation = Tween<double>(begin: 1.0, end: 1.02).animate(
|
|
CurvedAnimation(parent: _animationController, curve: Curves.easeOut),
|
|
);
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_animationController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return MouseRegion(
|
|
onEnter: (_) {
|
|
setState(() => _isHovered = true);
|
|
_animationController.forward();
|
|
},
|
|
onExit: (_) {
|
|
setState(() => _isHovered = false);
|
|
_animationController.reverse();
|
|
},
|
|
child: ScaleTransition(
|
|
scale: _scaleAnimation,
|
|
child: GestureDetector(
|
|
onTap: widget.onTap,
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// Album Art
|
|
Stack(
|
|
children: [
|
|
Container(
|
|
width: double.infinity,
|
|
aspectRatio: 1,
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.circular(12),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.3),
|
|
blurRadius: 20,
|
|
offset: const Offset(0, 4),
|
|
),
|
|
],
|
|
),
|
|
child: ClipRRect(
|
|
borderRadius: BorderRadius.circular(12),
|
|
child: Image.network(
|
|
widget.imageUrl,
|
|
fit: BoxFit.cover,
|
|
errorBuilder: (context, error, stackTrace) {
|
|
return Container(
|
|
color: AppColors.surface,
|
|
child: Icon(
|
|
Icons.music_note,
|
|
size: 48,
|
|
color: AppColors.textTertiary,
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
// Play Overlay
|
|
if (_isHovered)
|
|
Positioned.fill(
|
|
child: Container(
|
|
decoration: BoxDecoration(
|
|
color: AppColors.background.withOpacity(0.7),
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
child: Center(
|
|
child: Container(
|
|
width: 56,
|
|
height: 56,
|
|
decoration: BoxDecoration(
|
|
color: AppColors.primary,
|
|
shape: BoxShape.circle,
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: AppColors.primary.withOpacity(0.4),
|
|
blurRadius: 24,
|
|
offset: const Offset(0, 4),
|
|
),
|
|
],
|
|
),
|
|
child: Icon(
|
|
Icons.play_arrow,
|
|
color: AppColors.textInverted,
|
|
size: 32,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 12),
|
|
// Title
|
|
Text(
|
|
widget.title,
|
|
style: AppTypography.h3.copyWith(
|
|
fontSize: 16,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
maxLines: 1,
|
|
),
|
|
const SizedBox(height: 4),
|
|
// Subtitle
|
|
Text(
|
|
widget.subtitle,
|
|
style: AppTypography.bodySmall,
|
|
maxLines: 1,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Migration du Code Existant
|
|
|
|
### Priorités de Migration
|
|
|
|
1. **Phase 1** - Fondation (Priorité Haute)
|
|
- ✅ Implémenter `colors.dart`
|
|
- ✅ Implémenter `typography.dart`
|
|
- ✅ Implémenter `app_theme.dart`
|
|
- ✅ Mettre à jour `main.dart` avec le nouveau thème
|
|
|
|
2. **Phase 2** - Composants (Priorité Haute)
|
|
- ✅ Créer `PrimaryButton`
|
|
- ✅ Créer `AlbumCard`
|
|
- ✅ Créer `SearchInput`
|
|
- ✅ Créer `ProgressBar` (player)
|
|
|
|
3. **Phase 3** - Pages (Priorité Moyenne)
|
|
- ✅ Migrer la page Home
|
|
- ✅ Migrer la page Search
|
|
- ✅ Migrer la page Player
|
|
|
|
4. **Phase 4** - Finitions (Priorité Basse)
|
|
- Animations et transitions
|
|
- États de loading
|
|
- États empty
|
|
|
|
### Checklist par Page
|
|
|
|
#### Page Home
|
|
- [ ] Hero section avec gradient animé
|
|
- [ ] Quick picks grid
|
|
- [ ] Horizontal scroll rows
|
|
- [ ] Skeleton loading states
|
|
- [ ] Category pills
|
|
|
|
#### Page Search
|
|
- [ ] Search bar avec clear button
|
|
- [ ] Search tabs
|
|
- [ ] Recent searches
|
|
- [ ] Trending searches
|
|
- [ ] Results grid/list
|
|
|
|
#### Page Player
|
|
- [ ] Large album art avec glow
|
|
- [ ] Progress bar avec handle
|
|
- [ ] Control buttons (primary + secondary)
|
|
- [ ] Volume slider
|
|
- [ ] Queue panel
|
|
- [ ] Mini player sticky
|
|
|
|
---
|
|
|
|
## Checklist de Validation
|
|
|
|
Avant de considérer une page comme terminée, vérifiez :
|
|
|
|
### Visuel
|
|
- [ ] Pas d'emojis comme icônes (SVG seulement)
|
|
- [ ] Icônes cohérentes (Lucide/Heroicons)
|
|
- [ ] Hover states sans layout shift
|
|
- [ ] Couleurs du thème utilisées directement
|
|
- [ ] Effets néon subtils, pas écrasants
|
|
|
|
### Interaction
|
|
- [ ] Tous les éléments cliquables ont `cursor: pointer`
|
|
- [ ] Hover states fournissent feedback clair
|
|
- [ ] Transitions 150-300ms
|
|
- [ ] Focus states visibles
|
|
|
|
### Accessibilité
|
|
- [ ] Contraste texte minimum 4.5:1
|
|
- [ ] Toutes les images ont alt text
|
|
- [ ] Inputs ont labels
|
|
- [ ] Tabulation fonctionne
|
|
- [ ] `prefers-reduced-motion` respecté
|
|
|
|
### Responsive
|
|
- [ ] Fonctionne à 375px (mobile)
|
|
- [ ] Fonctionne à 768px (tablet)
|
|
- [ ] Fonctionne à 1024px (desktop)
|
|
- [ ] Pas de scroll horizontal mobile
|
|
- [ ] Touch targets min 44x44px
|
|
|
|
### Performance
|
|
- [ ] Images WebP avec fallbacks
|
|
- [ ] Lazy loading pour images larges
|
|
- [ ] Animations utilisent transform/opacity
|
|
- [ ] Pas de layout shifts
|
|
|
|
---
|
|
|
|
## Ressources Utiles
|
|
|
|
### Fonts Google
|
|
- **Space Grotesk**: https://fonts.google.com/specimen/Space+Grotesk
|
|
- **Outfit**: https://fonts.google.com/specimen/Outfit
|
|
- **JetBrains Mono**: https://fonts.google.com/specimen/JetBrains+Mono
|
|
|
|
### Icônes
|
|
- **Lucide Icons**: https://lucide.dev/
|
|
- **Heroicons**: https://heroicons.com/
|
|
|
|
### Outils de Contraste
|
|
- **WebAIM Contrast Checker**: https://webaim.org/resources/contrastchecker/
|
|
|
|
### Documentation Flutter
|
|
- **Theme Data**: https://api.flutter.dev/flutter/material/ThemeData-class.html
|
|
- **Animation Controller**: https://api.flutter.dev/flutter/animation/AnimationController-class.html
|
|
|
|
---
|
|
|
|
## Prochaines Étapes
|
|
|
|
1. ✅ **Design system créé** - MASTER.md + overrides
|
|
2. 🔄 **Implémenter les fichiers de thème** - colors, typography, app_theme
|
|
3. 🔄 **Créer les composants de base** - buttons, cards, inputs
|
|
4. 🔄 **Migrer page par page** - Commencer par Home
|
|
5. 🔄 **Tester et valider** - Accessibilité, responsive, performance
|
|
|
|
---
|
|
|
|
**Besoin d'aide?** Référez-vous toujours aux fichiers dans `design-system/` pour les règles spécifiques à chaque page.
|