feat(19): restyle tablo cards to production design with X/Y task count

- TabloCardView gains DoneTasks/TotalTasks int fields; handler stores raw counts alongside Progress %
- TabloProjectCard: rounded-2xl card, border-[#EAECF0], purple-50 status badge, colored avatar initial (12x12 rounded-xl), calendar date row, Progression label + X/Y task count + purple-500 progress bar
- List row: matching pill badge + X/Y count
- All 7 TestTablosDashboard_* tests pass

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Arthur Belleville 2026-05-17 17:00:51 +02:00
parent 2a835f52e9
commit 7388418aed
No known key found for this signature in database
4 changed files with 54 additions and 41 deletions

View file

@ -63,14 +63,18 @@ func TablosListHandler(deps TablosDeps) http.HandlerFunc {
// Non-fatal: proceed with Progress = 0 for all cards.
progressRows = nil
}
progressMap := make(map[uuid.UUID]int, len(progressRows))
type progressEntry struct{ done, total int }
progressMap := make(map[uuid.UUID]progressEntry, len(progressRows))
for _, p := range progressRows {
if p.TotalTasks > 0 {
progressMap[p.TabloID] = int(p.DoneTasks * 100 / p.TotalTasks)
}
progressMap[p.TabloID] = progressEntry{done: int(p.DoneTasks), total: int(p.TotalTasks)}
}
for i := range cardViews {
cardViews[i].Progress = progressMap[cardViews[i].Tablo.ID]
e := progressMap[cardViews[i].Tablo.ID]
cardViews[i].DoneTasks = e.done
cardViews[i].TotalTasks = e.total
if e.total > 0 {
cardViews[i].Progress = e.done * 100 / e.total
}
}
sidebarTablos := make([]sqlc.Tablo, 0, len(cardViews))

File diff suppressed because one or more lines are too long

View file

@ -37,6 +37,8 @@ type TabloCardView struct {
Tablo sqlc.Tablo
DiscussionUnreadCount int64
Progress int // 0100; 0 when no tasks (D-05)
DoneTasks int // raw count of done tasks
TotalTasks int // raw count of all tasks
}
func DiscussionPostURL(tabloID uuid.UUID) string {

View file

@ -93,16 +93,16 @@ templ TablosEmptyState() {
templ TabloProjectCard(card TabloCardView, csrfToken string) {
<article id={ "tablo-" + card.Tablo.ID.String() } class="tablo-card-wrapper">
<!-- Card view (default: visible in grid layout) -->
<div class="project-card">
<div class="project-card-top">
<!-- Status badge top-left -->
<div class="bg-white rounded-2xl p-4 border border-[#EAECF0] hover:shadow-md transition-shadow cursor-pointer project-card">
<!-- Top row: status badge + delete button -->
<div class="flex items-start justify-between mb-4">
if len(card.Tablo.Status) > 0 {
@ui.Badge(ui.BadgeProps{
Label: strings.ToUpper(card.Tablo.Status[:1]) + card.Tablo.Status[1:],
Variant: ui.BadgeVariantPrimary,
})
<span class="px-3 py-1 rounded-full text-xs font-medium border bg-purple-50 text-purple-600 border-purple-200">
{ strings.ToUpper(card.Tablo.Status[:1]) + card.Tablo.Status[1:] }
</span>
} else {
<span></span>
}
<!-- Delete button top-right -->
<div class="tablo-delete-zone">
@ui.IconButton(ui.IconButtonProps{
Label: "Delete tablo",
@ -118,46 +118,53 @@ templ TabloProjectCard(card TabloCardView, csrfToken string) {
})
</div>
</div>
<div class="project-card-title-row">
<!-- Avatar + title -->
<div class="flex items-center gap-3 mb-4">
if card.Tablo.Color.Valid && card.Tablo.Color.String != "" {
<span class="project-avatar" style={ "background-color: " + card.Tablo.Color.String }>
if len(card.Tablo.Title) > 0 {
{ string([]rune(card.Tablo.Title)[0:1]) }
}
</span>
<div class="w-12 h-12 rounded-xl flex items-center justify-center shrink-0 overflow-hidden" style={ "background-color: " + card.Tablo.Color.String }>
<span class="text-white font-bold text-lg">
if len(card.Tablo.Title) > 0 {
{ string([]rune(card.Tablo.Title)[0:1]) }
}
</span>
</div>
} else {
<span class="project-avatar">
if len(card.Tablo.Title) > 0 {
{ string([]rune(card.Tablo.Title)[0:1]) }
}
</span>
<div class="w-12 h-12 rounded-xl flex items-center justify-center shrink-0 overflow-hidden bg-purple-500">
<span class="text-white font-bold text-lg">
if len(card.Tablo.Title) > 0 {
{ string([]rune(card.Tablo.Title)[0:1]) }
}
</span>
</div>
}
<div class="tablo-title-zone">
<h4>{ card.Tablo.Title }</h4>
<h3 class="font-semibold text-gray-900 flex-1 truncate">{ card.Tablo.Title }</h3>
</div>
<!-- Date row -->
<div class="flex items-center gap-2 text-sm text-gray-500 mb-4">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="w-4 h-4" 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>
<span>{ card.Tablo.CreatedAt.Time.Format("Jan 2, 2006") }</span>
</div>
<!-- Progress row -->
<div class="mb-3 project-card-progress-row">
<div class="flex items-center justify-between text-sm mb-2">
<span class="text-gray-600">Progression:</span>
<span class="font-semibold text-gray-900">{ strconv.Itoa(card.DoneTasks) }/{ strconv.Itoa(card.TotalTasks) }</span>
</div>
</div>
<div class="project-date-row">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" style="width:1rem;height:1rem;flex-shrink:0"><rect width="18" height="18" x="3" y="4" rx="2" ry="2"></rect><line x1="16" x2="16" y1="2" y2="6"></line><line x1="8" x2="8" y1="2" y2="6"></line><line x1="3" x2="21" y1="10" y2="10"></line></svg>
{ card.Tablo.CreatedAt.Time.Format("Jan 2, 2006") }
</div>
<div class="project-card-progress-row">
<span class="project-card-progress-label">Progression: { strconv.Itoa(card.Progress) }%</span>
<div class="project-progress-track">
<div class="project-card-progress-bar" style={ "width: " + strconv.Itoa(card.Progress) + "%" }></div>
<div class="w-full bg-gray-100 rounded-full h-2 project-progress-track">
<div class="h-2 rounded-full bg-purple-500 project-card-progress-bar" style={ "width: " + strconv.Itoa(card.Progress) + "%" }></div>
</div>
</div>
</div>
<!-- List row (hidden by default, shown when data-view="list") -->
<div class="tablo-list-row">
if len(card.Tablo.Status) > 0 {
@ui.Badge(ui.BadgeProps{
Label: strings.ToUpper(card.Tablo.Status[:1]) + card.Tablo.Status[1:],
Variant: ui.BadgeVariantPrimary,
})
<span class="px-2 py-0.5 rounded-full text-xs font-medium border bg-purple-50 text-purple-600 border-purple-200">
{ strings.ToUpper(card.Tablo.Status[:1]) + card.Tablo.Status[1:] }
</span>
}
<span class="tablo-list-row-title">{ card.Tablo.Title }</span>
<span class="tablo-list-row-meta">{ card.Tablo.CreatedAt.Time.Format("Jan 2, 2006") }</span>
<span class="tablo-list-row-meta">{ strconv.Itoa(card.Progress) }%</span>
<span class="tablo-list-row-meta">{ strconv.Itoa(card.DoneTasks) }/{ strconv.Itoa(card.TotalTasks) }</span>
<div class="tablo-delete-zone">
@ui.IconButton(ui.IconButtonProps{
Label: "Delete tablo",