feat(19): list view as proper table — Projet/Statut/Créé le/Progression/Actions

- Two separate containers: #tablos-grid (card grid) and #tablos-table (table), toggled via .hidden
- TabloListRow: new templ for <tr> with icon+title, Actif/Archivé badge, date, progress bar, trash
- JS: setTablosView toggles hidden on both containers; filterTablos targets both article and tr elements
- Remove old .tablo-list-row / #tablos-list[data-view] CSS (no longer needed)

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Arthur Belleville 2026-05-17 22:24:45 +02:00
parent 90d43f98bc
commit a575ab93c6
No known key found for this signature in database
3 changed files with 96 additions and 77 deletions

View file

@ -844,57 +844,7 @@
height: 100%;
}
/* Transparent grid-item wrapper — children participate in grid/flex directly */
.tablo-card-wrapper {
display: contents;
}
/* List-view row (hidden by default) */
.tablo-list-row {
display: none;
align-items: center;
gap: 1rem;
padding: 0.75rem 1rem;
border: 1px solid var(--color-border-subtle);
border-radius: 0.75rem;
background: var(--color-surface-default);
}
/* Title cell in list row */
.tablo-list-row-title {
flex: 1;
font-weight: 500;
}
/* Metadata cells in list row */
.tablo-list-row-meta {
color: var(--color-text-muted);
display: flex;
font-size: 0.875rem;
gap: 0.5rem;
}
/* data-view="list" toggle rules */
#tablos-list[data-view="list"] {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
#tablos-list[data-view="list"] .project-card {
display: none;
}
#tablos-list[data-view="list"] .tablo-list-row {
display: flex;
align-items: center;
}
/* Single-column layout in list view */
#tablos-list[data-view="list"] {
grid-template-columns: 1fr;
gap: 0.25rem;
}
/* Grid/table view is toggled via .hidden Tailwind class on #tablos-grid / #tablos-table */
/* ── View toggle tabs ─────────────────────────────────────── */

File diff suppressed because one or more lines are too long

View file

@ -49,8 +49,8 @@ templ TablosDashboard(user *auth.User, csrfToken string, activePath string, tabl
<button type="button" class="filter-tab" data-filter-btn="active" onclick="filterTablos('active')">Actif</button>
<button type="button" class="filter-tab" data-filter-btn="archived" onclick="filterTablos('archived')">Archivé</button>
</div>
<!-- Card/list grid -->
<div id="tablos-list" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4 sm:gap-6" data-view="grid">
<!-- Grid view -->
<div id="tablos-grid" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4 sm:gap-6">
if len(cards) == 0 {
@TablosEmptyState()
} else {
@ -59,28 +59,52 @@ templ TablosDashboard(user *auth.User, csrfToken string, activePath string, tabl
}
}
</div>
<!-- List view (hidden by default) -->
<div id="tablos-table" class="hidden bg-white rounded-xl border border-[#EAECF0] overflow-x-auto">
<table class="w-full min-w-[600px]">
<thead class="bg-gray-50 border-b border-[#EAECF0]">
<tr>
<th class="px-6 py-3 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">Projet</th>
<th class="px-6 py-3 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">Statut</th>
<th class="px-6 py-3 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">Créé le</th>
<th class="px-6 py-3 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">Progression</th>
<th class="px-6 py-3 w-12"></th>
</tr>
</thead>
<tbody>
for _, card := range cards {
@TabloListRow(card, csrfToken)
}
</tbody>
</table>
</div>
</div>
<script>
function setTablosView(v) {
document.getElementById('tablos-list').dataset.view = v;
document.getElementById('tablos-grid').classList.toggle('hidden', v === 'list');
document.getElementById('tablos-table').classList.toggle('hidden', v === 'grid');
document.querySelectorAll('[data-view-btn]').forEach(function(b) {
b.classList.toggle('is-active', b.dataset.viewBtn === v);
});
}
function filterTablos(s) {
document.querySelectorAll('#tablos-list article').forEach(function(el) {
document.querySelectorAll('#tablos-grid article').forEach(function(el) {
el.style.display = (s === 'tous' || el.dataset.displayStatus === s) ? '' : 'none';
});
document.querySelectorAll('#tablos-table tbody tr').forEach(function(el) {
el.style.display = (s === 'tous' || el.dataset.displayStatus === s) ? '' : 'none';
});
document.querySelectorAll('[data-filter-btn]').forEach(function(b) {
b.classList.toggle('is-active', b.dataset.filterBtn === s);
});
}
// Card click → navigate to tablo detail (stop on delete zone)
document.getElementById('tablos-list').addEventListener('click', function(e) {
var article = e.target.closest('article[data-href]');
if (article && !e.target.closest('.tablo-delete-zone')) {
window.location = article.dataset.href;
}
document.getElementById('tablos-grid').addEventListener('click', function(e) {
var a = e.target.closest('article[data-href]');
if (a && !e.target.closest('.tablo-delete-zone')) window.location = a.dataset.href;
});
document.getElementById('tablos-table').addEventListener('click', function(e) {
var r = e.target.closest('tr[data-href]');
if (r && !e.target.closest('.tablo-delete-zone')) window.location = r.dataset.href;
});
</script>
}
@ -189,21 +213,66 @@ templ TabloProjectCard(card TabloCardView, csrfToken string) {
</div>
</div>
</div>
<!-- List row (hidden by default, shown when data-view="list") -->
<div class="tablo-list-row" onclick="event.stopPropagation()">
<a class="flex items-center gap-4 w-full px-4 py-3 hover:bg-gray-50" href={ templ.SafeURL("/tablos/" + card.Tablo.ID.String()) }>
if tabloDisplayStatus(card.Progress, card.TotalTasks) == "termine" {
<span class="px-2 py-0.5 rounded-full text-xs font-medium bg-green-50 text-green-600 border border-green-200 shrink-0">Terminé</span>
} else if tabloDisplayStatus(card.Progress, card.TotalTasks) == "en-cours" {
<span class="px-2 py-0.5 rounded-full text-xs font-medium bg-[#FFF4E2] text-[#DB9729] border border-[#DB9729] shrink-0">En cours</span>
} else {
<span class="px-2 py-0.5 rounded-full text-xs font-medium bg-blue-50 text-blue-600 border border-blue-200 shrink-0">À faire</span>
</article>
}
<span class="tablo-list-row-title flex-1">{ card.Tablo.Title }</span>
<span class="tablo-list-row-meta shrink-0">{ card.Tablo.CreatedAt.Time.Format("2 Jan 2006") }</span>
<span class="tablo-list-row-meta shrink-0">{ strconv.Itoa(card.DoneTasks) }/{ strconv.Itoa(card.TotalTasks) }</span>
</a>
<div class="tablo-delete-zone shrink-0" onclick="event.stopPropagation()">
// TabloListRow renders one table row for the list view.
templ TabloListRow(card TabloCardView, csrfToken string) {
<tr
class="border-t border-[#EAECF0] hover:bg-gray-50 transition-colors cursor-pointer"
data-display-status={ card.Tablo.Status }
data-href={ "/tablos/" + card.Tablo.ID.String() }
>
<!-- Projet: icon + title -->
<td class="px-6 py-4">
<div class="flex items-center gap-3">
if card.Tablo.Color.Valid && card.Tablo.Color.String != "" {
<div class="w-8 h-8 rounded-lg flex items-center justify-center shrink-0 overflow-hidden" style={ "background-color: " + card.Tablo.Color.String }>
<span class="text-white font-bold text-sm">
if len(card.Tablo.Title) > 0 {
{ string([]rune(card.Tablo.Title)[0:1]) }
}
</span>
</div>
} else {
<div class="w-8 h-8 rounded-lg flex items-center justify-center shrink-0 overflow-hidden bg-blue-500">
<span class="text-white font-bold text-sm">
if len(card.Tablo.Title) > 0 {
{ string([]rune(card.Tablo.Title)[0:1]) }
}
</span>
</div>
}
<span class="font-medium text-gray-900 truncate">{ card.Tablo.Title }</span>
</div>
</td>
<!-- Statut -->
<td class="px-6 py-4 whitespace-nowrap">
if card.Tablo.Status == "archived" {
<span class="px-3 py-1 rounded-full text-sm font-medium bg-gray-100 text-gray-600 border border-gray-200">Archivé</span>
} else {
<span class="px-3 py-1 rounded-full text-sm font-medium bg-green-50 text-green-600 border border-green-200">Actif</span>
}
</td>
<!-- Créé le -->
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-600">
<div class="flex items-center gap-1.5">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="w-4 h-4 shrink-0" aria-hidden="true"><path d="M8 2v4"></path><path d="M16 2v4"></path><rect width="18" height="18" x="3" y="4" rx="2"></rect><path d="M3 10h18"></path></svg>
{ card.Tablo.CreatedAt.Time.Format("2 Jan 2006") }
</div>
</td>
<!-- Progression -->
<td class="px-6 py-4">
<div class="flex items-center gap-3">
<div class="flex-1 bg-gray-200 rounded-full h-2 min-w-[80px]">
<div class="bg-green-500 h-2 rounded-full" style={ "width: " + strconv.Itoa(card.Progress) + "%" }></div>
</div>
<span class="text-sm font-semibold text-gray-900 w-8 text-right">{ strconv.Itoa(card.Progress) }%</span>
</div>
</td>
<!-- Actions -->
<td class="px-6 py-4 text-right">
<div class="tablo-delete-zone inline-flex">
@ui.IconButton(ui.IconButtonProps{
Label: "Delete tablo",
Icon: "trash",
@ -217,8 +286,8 @@ templ TabloProjectCard(card TabloCardView, csrfToken string) {
},
})
</div>
</div>
</article>
</td>
</tr>
}
// TabloCard renders a single tablo as a ui.Card on the dashboard.