Bootstrap
Bootstrap5
Trello
Kanban
Dashboard
Html
Template Bootstrap 5 de tableau Kanban type Trello avec colonnes, cartes de taches et organisation visuelle.
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8" />
<meta name="copyright" content="MEZGANI Said" />
<meta name="author" content="AngularForAll" />
<meta name="robots" content="noindex, nofollow" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Template Trello Bootstrap 5 01 | AngularForAll</title>
<!-- Bootstrap 5 CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Bootstrap Icons -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
min-height: 100vh;
padding: 20px;
color: #172b4d;
}
/* Header style Trello */
.trello-header {
background: rgba(26, 26, 46, 0.8);
backdrop-filter: blur(10px);
padding: 12px 20px;
border-radius: 16px;
margin-bottom: 24px;
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 15px;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.board-title {
display: flex;
align-items: center;
gap: 12px;
}
.board-title i {
color: #00b4d8;
font-size: 1.8rem;
}
.board-title h1 {
color: white;
font-size: 1.5rem;
font-weight: 600;
margin: 0;
}
.board-actions {
display: flex;
gap: 10px;
}
.btn-trello {
background: rgba(255, 255, 255, 0.1);
color: white;
border: none;
padding: 8px 16px;
border-radius: 8px;
font-weight: 500;
transition: all 0.2s;
}
.btn-trello:hover {
background: rgba(255, 255, 255, 0.2);
color: white;
}
.btn-trello-primary {
background: #00b4d8;
color: white;
border: none;
padding: 8px 16px;
border-radius: 8px;
font-weight: 500;
transition: all 0.2s;
}
.btn-trello-primary:hover {
background: #0096c7;
}
/* Container des listes */
.lists-container {
display: flex;
gap: 20px;
overflow-x: auto;
padding-bottom: 20px;
min-height: calc(100vh - 180px);
}
/* Style d'une liste */
.list {
background: #ebecf0;
border-radius: 16px;
width: 300px;
min-width: 300px;
max-height: calc(100vh - 160px);
display: flex;
flex-direction: column;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.list-header {
padding: 16px 12px 8px;
display: flex;
align-items: center;
justify-content: space-between;
}
.list-header h3 {
font-size: 1rem;
font-weight: 600;
margin: 0;
color: #172b4d;
}
.list-actions {
display: flex;
gap: 5px;
}
.list-actions button {
background: none;
border: none;
color: #6b778c;
cursor: pointer;
padding: 4px 6px;
border-radius: 4px;
font-size: 1rem;
}
.list-actions button:hover {
background: rgba(0, 0, 0, 0.1);
color: #172b4d;
}
/* Container des cartes */
.cards-container {
padding: 8px 8px 4px;
flex: 1;
overflow-y: auto;
min-height: 50px;
}
/* Style d'une carte */
.card-item {
background: white;
border-radius: 10px;
padding: 12px 12px 8px;
margin-bottom: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
cursor: grab;
transition: all 0.2s;
border: 1px solid rgba(0, 0, 0, 0.05);
position: relative;
}
.card-item:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
transform: translateY(-2px);
}
.card-item.dragging {
opacity: 0.5;
cursor: grabbing;
}
.card-title {
font-weight: 500;
margin-bottom: 6px;
word-break: break-word;
}
.card-badges {
display: flex;
gap: 8px;
margin-bottom: 8px;
flex-wrap: wrap;
}
.badge-label {
font-size: 0.7rem;
padding: 2px 8px;
border-radius: 4px;
font-weight: 600;
color: white;
}
.badge-priority {
background: #eb5a46;
}
.badge-feature {
background: #00b4d8;
}
.badge-bug {
background: #f39c12;
}
.card-footer {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 8px;
}
.card-meta {
display: flex;
gap: 12px;
color: #6b778c;
font-size: 0.8rem;
}
.card-meta span {
display: flex;
align-items: center;
gap: 3px;
}
.card-avatar {
width: 24px;
height: 24px;
border-radius: 50%;
background: #00b4d8;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 0.7rem;
font-weight: 600;
}
/* Footer de liste (ajout de carte) */
.list-footer {
padding: 8px 12px 12px;
}
.add-card-btn {
width: 100%;
background: none;
border: none;
color: #6b778c;
padding: 8px 12px;
border-radius: 8px;
text-align: left;
font-size: 0.9rem;
transition: all 0.2s;
display: flex;
align-items: center;
gap: 8px;
}
.add-card-btn:hover {
background: rgba(0, 0, 0, 0.05);
color: #172b4d;
}
.add-card-form {
display: none;
}
.add-card-form.active {
display: block;
}
.add-card-form textarea {
width: 100%;
border: none;
border-radius: 8px;
padding: 8px 12px;
resize: none;
font-size: 0.9rem;
margin-bottom: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.add-card-form textarea:focus {
outline: 2px solid #00b4d8;
}
.form-actions {
display: flex;
align-items: center;
gap: 8px;
}
.form-actions button {
padding: 6px 12px;
}
/* Ajout de liste */
.add-list {
background: rgba(255, 255, 255, 0.1);
border-radius: 16px;
width: 300px;
min-width: 300px;
padding: 12px;
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.1);
height: fit-content;
}
.add-list-btn {
width: 100%;
background: none;
border: none;
color: white;
padding: 12px;
border-radius: 8px;
text-align: left;
font-size: 1rem;
transition: all 0.2s;
display: flex;
align-items: center;
gap: 8px;
}
.add-list-btn:hover {
background: rgba(255, 255, 255, 0.1);
}
.add-list-form {
display: none;
}
.add-list-form.active {
display: block;
}
.add-list-form input {
width: 100%;
border: none;
border-radius: 8px;
padding: 10px 12px;
font-size: 0.95rem;
margin-bottom: 8px;
}
/* Modal */
.modal-content {
border-radius: 16px;
border: none;
}
.modal-header {
border-bottom: 1px solid #e9ecef;
padding: 20px 24px;
}
.modal-body {
padding: 24px;
}
.modal-footer {
border-top: 1px solid #e9ecef;
padding: 16px 24px;
}
.priority-selector {
display: flex;
gap: 10px;
margin-top: 8px;
}
.priority-option {
flex: 1;
padding: 8px;
border-radius: 8px;
text-align: center;
cursor: pointer;
border: 2px solid transparent;
transition: all 0.2s;
}
.priority-option.selected {
border-color: #00b4d8;
}
.priority-high { background: #eb5a46; color: white; }
.priority-medium { background: #f39c12; color: white; }
.priority-low { background: #2ecc71; color: white; }
/* Scrollbar personnalisée */
.lists-container::-webkit-scrollbar {
height: 8px;
}
.lists-container::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.05);
border-radius: 10px;
}
.lists-container::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.2);
border-radius: 10px;
}
.cards-container::-webkit-scrollbar {
width: 5px;
}
.cards-container::-webkit-scrollbar-thumb {
background: #c1c7d0;
border-radius: 10px;
}
/* Responsive */
@media (max-width: 768px) {
body { padding: 12px; }
.trello-header {
padding: 10px 15px;
}
.board-title h1 {
font-size: 1.2rem;
}
.list {
width: 280px;
min-width: 280px;
}
.add-list {
width: 280px;
min-width: 280px;
}
}
/* Drag over effect */
.cards-container.drag-over {
background: rgba(0, 180, 216, 0.1);
border-radius: 8px;
}
</style>
</head>
<body>
<!-- Header -->
<div class="trello-header">
<div class="board-title">
<i class="bi bi-trello"></i>
<h1>Tableau Principal</h1>
<span class="badge bg-secondary" style="margin-left: 10px;">Kanban</span>
</div>
<div class="board-actions">
<button class="btn-trello" onclick="showBoardMenu()">
<i class="bi bi-three-dots"></i>
</button>
<button class="btn-trello-primary" onclick="exportBoard()">
<i class="bi bi-download"></i> Exporter
</button>
</div>
</div>
<!-- Container des listes -->
<div class="lists-container" id="listsContainer">
<!-- Les listes seront injectées ici par JavaScript -->
</div>
<!-- Bouton d'ajout de liste (fixe) -->
<div class="add-list" id="addListSection">
<button class="add-list-btn" id="showAddListBtn">
<i class="bi bi-plus-lg"></i> Ajouter une liste
</button>
<div class="add-list-form" id="addListForm">
<input type="text" id="newListTitle" placeholder="Titre de la liste...">
<div class="form-actions">
<button class="btn btn-primary btn-sm" onclick="addNewList()">Ajouter</button>
<button class="btn btn-link btn-sm text-white" onclick="hideAddListForm()">
<i class="bi bi-x-lg"></i>
</button>
</div>
</div>
</div>
<!-- Modal pour ajouter/modifier une carte -->
<div class="modal fade" id="cardModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="modalTitle">Ajouter une carte</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="cardForm">
<input type="hidden" id="cardId">
<input type="hidden" id="listId">
<div class="mb-3">
<label class="form-label fw-bold">Titre</label>
<input type="text" class="form-control" id="cardTitle" required placeholder="Titre de la carte">
</div>
<div class="mb-3">
<label class="form-label fw-bold">Description</label>
<textarea class="form-control" id="cardDescription" rows="3" placeholder="Description (optionnel)"></textarea>
</div>
<div class="mb-3">
<label class="form-label fw-bold">Type</label>
<select class="form-select" id="cardType">
<option value="feature">✨ Fonctionnalité</option>
<option value="bug">🐛 Bug</option>
<option value="task">📋 Tâche</option>
</select>
</div>
<div class="mb-3">
<label class="form-label fw-bold">Priorité</label>
<div class="priority-selector">
<div class="priority-option priority-high" data-priority="high" onclick="selectPriority('high')">Haute</div>
<div class="priority-option priority-medium selected" data-priority="medium" onclick="selectPriority('medium')">Moyenne</div>
<div class="priority-option priority-low" data-priority="low" onclick="selectPriority('low')">Basse</div>
</div>
<input type="hidden" id="cardPriority" value="medium">
</div>
<div class="mb-3">
<label class="form-label fw-bold">Assigné à</label>
<select class="form-select" id="cardAssignee">
<option value="Moi">👤 Moi</option>
<option value="Marie">👩 Marie</option>
<option value="Thomas">👨 Thomas</option>
<option value="Sophie">👩 Sophie</option>
</select>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Annuler</button>
<button type="button" class="btn btn-primary" onclick="saveCard()">Enregistrer</button>
</div>
</div>
</div>
</div>
<!-- Bootstrap JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
// ===== DONNÉES =====
let boardData = {
lists: [
{
id: 'list-1',
title: 'À faire',
cards: [
{ id: 'card-1', title: 'Design de la page d\'accueil', description: 'Créer la maquette Figma', type: 'feature', priority: 'high', assignee: 'Marie', comments: 2 },
{ id: 'card-2', title: 'Correction bug login', description: 'Erreur 500 sur l\'authentification', type: 'bug', priority: 'high', assignee: 'Thomas', comments: 5 }
]
},
{
id: 'list-2',
title: 'En cours',
cards: [
{ id: 'card-3', title: 'Développement API', description: 'Créer les endpoints REST', type: 'feature', priority: 'medium', assignee: 'Moi', comments: 3 },
{ id: 'card-4', title: 'Tests unitaires', description: 'Couvrir le code à 80%', type: 'task', priority: 'medium', assignee: 'Sophie', comments: 1 }
]
},
{
id: 'list-3',
title: 'Terminé',
cards: [
{ id: 'card-5', title: 'Mise en place CI/CD', description: 'GitHub Actions configuré', type: 'task', priority: 'low', assignee: 'Moi', comments: 0 },
{ id: 'card-6', title: 'Documentation', description: 'Guide d\'installation', type: 'task', priority: 'low', assignee: 'Marie', comments: 2 }
]
}
]
};
let draggedCard = null;
let cardModal;
let currentListId = null;
let editingCardId = null;
// ===== INITIALISATION =====
document.addEventListener('DOMContentLoaded', function() {
cardModal = new bootstrap.Modal(document.getElementById('cardModal'));
renderBoard();
setupEventListeners();
});
function setupEventListeners() {
document.getElementById('showAddListBtn').addEventListener('click', showAddListForm);
}
// ===== RENDU DU TABLEAU =====
function renderBoard() {
const container = document.getElementById('listsContainer');
let html = '';
boardData.lists.forEach(list => {
html += renderList(list);
});
container.innerHTML = html;
// Attacher les événements de drag & drop
attachDragDropEvents();
}
function renderList(list) {
const cardsHtml = list.cards.map(card => renderCard(card)).join('');
return `
<div class="list" data-list-id="${list.id}">
<div class="list-header">
<h3>${list.title} <span class="badge bg-secondary ms-2">${list.cards.length}</span></h3>
<div class="list-actions">
<button onclick="addCardToList('${list.id}')" title="Ajouter une carte">
<i class="bi bi-plus-lg"></i>
</button>
<button onclick="editList('${list.id}')" title="Modifier la liste">
<i class="bi bi-pencil"></i>
</button>
<button onclick="deleteList('${list.id}')" title="Supprimer la liste">
<i class="bi bi-trash"></i>
</button>
</div>
</div>
<div class="cards-container" data-list-id="${list.id}">
${cardsHtml}
</div>
<div class="list-footer">
<button class="add-card-btn" onclick="showAddCardForm('${list.id}')">
<i class="bi bi-plus-lg"></i> Ajouter une carte
</button>
<div class="add-card-form" id="addCardForm-${list.id}">
<textarea placeholder="Titre de la carte..." id="newCardTitle-${list.id}" rows="2"></textarea>
<div class="form-actions">
<button class="btn btn-primary btn-sm" onclick="quickAddCard('${list.id}')">Ajouter</button>
<button class="btn btn-link btn-sm" onclick="hideAddCardForm('${list.id}')">
<i class="bi bi-x-lg"></i>
</button>
</div>
</div>
</div>
</div>
`;
}
function renderCard(card) {
const typeBadge = getTypeBadge(card.type);
const priorityClass = `priority-${card.priority}`;
return `
<div class="card-item" draggable="true" data-card-id="${card.id}" data-list-id="${card.listId || ''}">
<div class="card-title">${card.title}</div>
<div class="card-badges">
<span class="badge-label ${typeBadge.class}">${typeBadge.label}</span>
<span class="badge-label bg-${getPriorityColor(card.priority)}">${card.priority}</span>
</div>
${card.description ? `<div style="font-size: 0.8rem; color: #6b778c; margin-bottom: 8px;">${card.description.substring(0, 50)}...</div>` : ''}
<div class="card-footer">
<div class="card-meta">
${card.comments > 0 ? `<span><i class="bi bi-chat"></i> ${card.comments}</span>` : ''}
<span><i class="bi bi-paperclip"></i></span>
</div>
<div class="card-avatar" title="${card.assignee}">
${card.assignee.charAt(0)}
</div>
</div>
<button class="btn btn-sm btn-outline-secondary mt-2" style="width: 100%;" onclick="editCard('${card.id}')">
<i class="bi bi-pencil"></i> Modifier
</button>
</div>
`;
}
function getTypeBadge(type) {
const types = {
feature: { label: 'Fonctionnalité', class: 'badge-feature' },
bug: { label: 'Bug', class: 'badge-bug' },
task: { label: 'Tâche', class: 'badge-label bg-secondary' }
};
return types[type] || types.task;
}
function getPriorityColor(priority) {
const colors = { high: 'danger', medium: 'warning', low: 'success' };
return colors[priority] || 'secondary';
}
// ===== DRAG & DROP =====
function attachDragDropEvents() {
const cards = document.querySelectorAll('.card-item');
const containers = document.querySelectorAll('.cards-container');
cards.forEach(card => {
card.addEventListener('dragstart', handleDragStart);
card.addEventListener('dragend', handleDragEnd);
});
containers.forEach(container => {
container.addEventListener('dragover', handleDragOver);
container.addEventListener('dragenter', handleDragEnter);
container.addEventListener('dragleave', handleDragLeave);
container.addEventListener('drop', handleDrop);
});
}
function handleDragStart(e) {
const card = e.target.closest('.card-item');
draggedCard = {
id: card.dataset.cardId,
sourceListId: card.closest('.cards-container').dataset.listId,
element: card
};
card.classList.add('dragging');
e.dataTransfer.effectAllowed = 'move';
}
function handleDragEnd(e) {
e.target.classList.remove('dragging');
document.querySelectorAll('.cards-container').forEach(c => {
c.classList.remove('drag-over');
});
}
function handleDragOver(e) {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
}
function handleDragEnter(e) {
e.preventDefault();
e.target.closest('.cards-container')?.classList.add('drag-over');
}
function handleDragLeave(e) {
e.target.closest('.cards-container')?.classList.remove('drag-over');
}
function handleDrop(e) {
e.preventDefault();
const container = e.target.closest('.cards-container');
container.classList.remove('drag-over');
const targetListId = container.dataset.listId;
if (draggedCard && draggedCard.sourceListId !== targetListId) {
// Trouver la carte dans la liste source
const sourceList = boardData.lists.find(l => l.id === draggedCard.sourceListId);
const targetList = boardData.lists.find(l => l.id === targetListId);
if (sourceList && targetList) {
const cardIndex = sourceList.cards.findIndex(c => c.id === draggedCard.id);
if (cardIndex !== -1) {
const [movedCard] = sourceList.cards.splice(cardIndex, 1);
targetList.cards.push(movedCard);
renderBoard();
}
}
}
draggedCard = null;
}
// ===== GESTION DES LISTES =====
function showAddListForm() {
document.getElementById('addListForm').classList.add('active');
document.getElementById('showAddListBtn').style.display = 'none';
document.getElementById('newListTitle').focus();
}
function hideAddListForm() {
document.getElementById('addListForm').classList.remove('active');
document.getElementById('showAddListBtn').style.display = 'block';
document.getElementById('newListTitle').value = '';
}
function addNewList() {
const title = document.getElementById('newListTitle').value.trim();
if (!title) return;
const newList = {
id: 'list-' + Date.now(),
title: title,
cards: []
};
boardData.lists.push(newList);
renderBoard();
hideAddListForm();
}
function deleteList(listId) {
if (confirm('Supprimer cette liste et toutes ses cartes ?')) {
boardData.lists = boardData.lists.filter(l => l.id !== listId);
renderBoard();
}
}
function editList(listId) {
const list = boardData.lists.find(l => l.id === listId);
if (!list) return;
const newTitle = prompt('Nouveau titre de la liste:', list.title);
if (newTitle && newTitle.trim()) {
list.title = newTitle.trim();
renderBoard();
}
}
// ===== GESTION DES CARTES =====
function showAddCardForm(listId) {
document.getElementById(`addCardForm-${listId}`).classList.add('active');
document.querySelector(`[onclick="showAddCardForm('${listId}')"]`).style.display = 'none';
document.getElementById(`newCardTitle-${listId}`).focus();
}
function hideAddCardForm(listId) {
document.getElementById(`addCardForm-${listId}`).classList.remove('active');
document.querySelector(`[onclick="showAddCardForm('${listId}')"]`).style.display = 'block';
document.getElementById(`newCardTitle-${listId}`).value = '';
}
function quickAddCard(listId) {
const title = document.getElementById(`newCardTitle-${listId}`).value.trim();
if (!title) return;
const list = boardData.lists.find(l => l.id === listId);
if (list) {
const newCard = {
id: 'card-' + Date.now(),
title: title,
description: '',
type: 'task',
priority: 'medium',
assignee: 'Moi',
comments: 0
};
list.cards.push(newCard);
renderBoard();
}
hideAddCardForm(listId);
}
function addCardToList(listId) {
currentListId = listId;
editingCardId = null;
document.getElementById('modalTitle').textContent = 'Ajouter une carte';
document.getElementById('cardForm').reset();
document.getElementById('listId').value = listId;
document.getElementById('cardPriority').value = 'medium';
document.querySelectorAll('.priority-option').forEach(opt => {
opt.classList.remove('selected');
if (opt.dataset.priority === 'medium') {
opt.classList.add('selected');
}
});
cardModal.show();
}
function editCard(cardId) {
let foundCard = null;
let foundList = null;
for (const list of boardData.lists) {
const card = list.cards.find(c => c.id === cardId);
if (card) {
foundCard = card;
foundList = list;
break;
}
}
if (!foundCard) return;
editingCardId = cardId;
currentListId = foundList.id;
document.getElementById('modalTitle').textContent = 'Modifier la carte';
document.getElementById('cardId').value = cardId;
document.getElementById('listId').value = foundList.id;
document.getElementById('cardTitle').value = foundCard.title;
document.getElementById('cardDescription').value = foundCard.description || '';
document.getElementById('cardType').value = foundCard.type;
document.getElementById('cardPriority').value = foundCard.priority;
document.getElementById('cardAssignee').value = foundCard.assignee;
document.querySelectorAll('.priority-option').forEach(opt => {
opt.classList.remove('selected');
if (opt.dataset.priority === foundCard.priority) {
opt.classList.add('selected');
}
});
cardModal.show();
}
function selectPriority(priority) {
document.querySelectorAll('.priority-option').forEach(opt => {
opt.classList.remove('selected');
if (opt.dataset.priority === priority) {
opt.classList.add('selected');
}
});
document.getElementById('cardPriority').value = priority;
}
function saveCard() {
const cardId = document.getElementById('cardId').value;
const listId = document.getElementById('listId').value;
const title = document.getElementById('cardTitle').value.trim();
const description = document.getElementById('cardDescription').value.trim();
const type = document.getElementById('cardType').value;
const priority = document.getElementById('cardPriority').value;
const assignee = document.getElementById('cardAssignee').value;
if (!title) {
alert('Le titre est obligatoire');
return;
}
const list = boardData.lists.find(l => l.id === listId);
if (!list) return;
if (cardId) {
const card = list.cards.find(c => c.id === cardId);
if (card) {
card.title = title;
card.description = description;
card.type = type;
card.priority = priority;
card.assignee = assignee;
}
} else {
const newCard = {
id: 'card-' + Date.now(),
title: title,
description: description,
type: type,
priority: priority,
assignee: assignee,
comments: 0
};
list.cards.push(newCard);
}
renderBoard();
cardModal.hide();
}
// ===== FONCTIONS UTILITAIRES =====
function showBoardMenu() {
alert('Menu du tableau\n- Paramètres\n- Activité\n- Partager');
}
function exportBoard() {
const dataStr = JSON.stringify(boardData, null, 2);
const dataUri = 'data:application/json;charset=utf-8,'+ encodeURIComponent(dataStr);
const exportFileDefaultName = 'trello-board.json';
const linkElement = document.createElement('a');
linkElement.setAttribute('href', dataUri);
linkElement.setAttribute('download', exportFileDefaultName);
linkElement.click();
}
console.log('✅ Trello Clone - Prêt !');
</script>
</body>
</html>
Ouvrir cet aperçu dans un nouvel onglet du navigateur
🔗 Ouvrir dans le navigateur