feat(phase-20): port tablo detail restyle to backend/

Phase 20 work was executed against go-backend/ (prototype) instead of
backend/ (production). This commit ports the Figma-aligned restyle to
the correct codebase.

Changes:
- tablos.templ: replace project-card-top header with tablo-detail-header,
  tablo-detail-avatar, tablo-detail-title; update tab bar to tablo-tab-bar;
  localise tab labels to French (Vue d'ensemble, Tâches, Fichiers,
  Discussion, Événements)
- tasks.templ: update KanbanBoard to tablo-kanban-board; KanbanColumn to
  tablo-kanban-column with new header/count/empty classes; TaskCard to
  card-style layout (task-card-top-row, task-card-title, task-card-delete)
- app.css: add sections 25–27 — tablo detail page, kanban board/columns,
  and task card (card appearance, hover states)

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Arthur Belleville 2026-05-18 16:31:05 +02:00
parent efa9a85dd7
commit 63e7d65290
No known key found for this signature in database
3 changed files with 347 additions and 187 deletions

View file

@ -1107,3 +1107,195 @@
border: 1px solid var(--color-border-default, #e2e8f0);
color: var(--color-text-primary);
}
/* ============================================================
Section 25 Tablo detail page (Figma restyle)
============================================================ */
.tablo-detail-page {
padding: 24px 32px;
}
.tablo-detail-header {
border-bottom: 1px solid var(--color-border-muted);
display: flex;
flex-direction: column;
gap: 0;
padding-bottom: 0;
}
.tablo-detail-title-row {
align-items: center;
display: flex;
gap: 16px;
padding-bottom: 16px;
}
.tablo-detail-avatar {
align-items: center;
background: var(--color-surface-muted);
border-radius: 12px;
color: #ffffff;
display: flex;
flex-shrink: 0;
font-size: 1rem;
font-weight: 700;
height: 48px;
justify-content: center;
width: 48px;
}
.tablo-detail-title {
color: var(--color-text-primary);
font-size: 1.75rem;
font-weight: 600;
line-height: 1.2;
margin: 0;
}
/* Override the existing tablo-metadata-row for the detail page context */
.tablo-detail-header .tablo-metadata-row {
border-bottom: 1px solid var(--color-border-muted);
gap: 24px;
margin-bottom: 0;
padding-block: 16px;
}
.tablo-meta-segment {
align-items: center;
color: var(--color-text-secondary);
display: flex;
font-size: 0.875rem;
gap: 8px;
}
.tablo-meta-progress {
align-items: center;
display: flex;
gap: 8px;
}
.tablo-progress-bar {
background: var(--color-brand-primary);
border-radius: 9999px;
height: 5px;
}
.tablo-tab-bar {
border-bottom: 1px solid var(--color-border-muted);
display: flex;
gap: 24px;
padding-top: 8px;
}
.tablo-tab-bar .tab-nav-item {
font-size: 0.875rem;
padding-bottom: 12px;
}
/* ============================================================
Section 26 Kanban board & columns (Figma restyle)
============================================================ */
.tablo-kanban-board {
display: flex;
gap: 16px;
overflow-x: auto;
padding-bottom: 16px;
padding-top: 24px;
}
.tablo-kanban-column {
background: var(--color-surface-default);
border: 1px solid var(--color-border-default);
border-radius: 0.75rem;
flex-shrink: 0;
overflow: hidden;
width: 18rem;
}
.tablo-kanban-column-header {
align-items: center;
background: var(--color-surface-muted);
display: flex;
gap: 8px;
padding: 12px 16px;
}
.tablo-kanban-column-title {
color: var(--color-text-primary);
font-size: 0.875rem;
font-weight: 600;
margin: 0;
}
.tablo-kanban-task-count {
background: var(--color-surface-default);
border: 1px solid var(--color-border-muted);
border-radius: 9999px;
color: var(--color-text-secondary);
font-size: 0.75rem;
padding: 0 8px;
}
.tablo-kanban-empty {
color: var(--color-text-faint);
font-size: 0.875rem;
padding: 24px 16px;
text-align: center;
}
/* ============================================================
Section 27 Task card (kanban card style)
============================================================ */
.task-card {
background: var(--color-surface-default);
border: 1px solid var(--color-border-default);
border-radius: 8px;
cursor: pointer;
display: flex;
flex-direction: column;
gap: 8px;
margin: 8px;
padding-block: 8px;
padding-inline: 12px;
transition: box-shadow 0.12s ease, border-color 0.12s ease;
}
.task-card:hover {
border-color: var(--color-border-strong);
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.08);
}
.task-card-top-row {
align-items: flex-start;
display: flex;
gap: 6px;
}
.task-card-title {
color: var(--color-text-primary);
flex: 1;
font-size: 0.875rem;
font-weight: 400;
line-height: 1.4;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.task-card-delete {
flex-shrink: 0;
opacity: 0;
transition: opacity 0.12s ease;
}
.task-card:hover .task-card-delete {
opacity: 1;
}
.task-card:hover .task-drag-handle {
opacity: 1;
}

View file

@ -406,58 +406,33 @@ templ TabloCardWithOOBFormClear(tablo sqlc.Tablo, csrfToken string) {
<div id="create-form-slot" hx-swap-oob="true"></div>
}
// TabloDetailPage renders the full detail page for a single tablo with a 3-tab layout.
// Tabs: Overview / Tasks / Files. activeTab selects the initially rendered tab content.
// TabloDetailPage renders the full detail page for a single tablo.
// Tabs: Overview / Tasks / Files / Discussion / Events. activeTab selects the initially rendered tab content.
// files and tasks are pre-fetched slices for the active tab (may be nil for inactive tabs).
// activePath and sidebarTablos drive the AppLayout sidebar.
// UI-SPEC §3 Interaction Contract — GET /tablos/{id}.
// D-07: signature includes activeTab string param; D-08: tab bar links carry hx-push-url.
templ TabloDetailPage(user *auth.User, csrfToken string, activePath string, sidebarTablos []sqlc.Tablo, tablo sqlc.Tablo, tasks []sqlc.Task, etapes []sqlc.Etape, counts EtapeTaskCounts, filter EtapeFilter, files []sqlc.TabloFile, events EventsCalendar, discussion DiscussionTabData, activeTab string, pageTitle string, breadcrumb []BreadcrumbItem) {
@AppLayout("Tablos — Xtablo", user, csrfToken, activePath, sidebarTablos, pageTitle, breadcrumb, nil) {
<!-- Header: project-card-top layout with color avatar, title zone, and action controls -->
<div class="px-4 pt-4">
<div class="project-card-top">
<div class="project-card-title-row">
<div class="tablo-detail-page">
<header class="tablo-detail-header">
<div class="tablo-detail-title-row">
if tablo.Color.Valid && tablo.Color.String != "" {
<span class="project-avatar" style={ "background-color: " + tablo.Color.String }>
<div class="tablo-detail-avatar" style={ "background:" + tablo.Color.String }>
if len(tablo.Title) > 0 {
{ string([]rune(tablo.Title)[0:1]) }
}
</span>
</div>
} else {
<span class="project-avatar">
<div class="tablo-detail-avatar">
if len(tablo.Title) > 0 {
{ string([]rune(tablo.Title)[0:1]) }
}
</span>
</div>
}
<div class="tablo-title-zone">
@TabloTitleDisplay(tablo, csrfToken)
</div>
</div>
<div class="flex items-center gap-3">
<a
href={ templ.SafeURL("/tablos/" + tablo.ID.String() + "/discussion") }
hx-get={ "/tablos/" + tablo.ID.String() + "/discussion" }
hx-target="#tab-content"
hx-swap="innerHTML"
hx-push-url={ "/tablos/" + tablo.ID.String() + "/discussion" }
>
@ui.IconButton(ui.IconButtonProps{
Label: "Discussion",
Icon: "chat",
Variant: ui.IconButtonVariantNeutral,
Tone: ui.IconButtonToneGhost,
Type: "button",
})
</a>
@ui.Button(ui.ButtonProps{
Label: "Invite Member",
Variant: ui.ButtonVariantDefault,
Tone: ui.ButtonToneSoft,
Size: ui.SizeMD,
Type: "button",
})
<div class="tablo-delete-zone">
@ui.IconButton(ui.IconButtonProps{
Label: "Delete tablo",
@ -473,116 +448,115 @@ templ TabloDetailPage(user *auth.User, csrfToken string, activePath string, side
})
</div>
</div>
</div>
<!-- Metadata row: created date, status, progress -->
<div class="tablo-metadata-row">
<div class="tablo-metadata-date">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><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>
<span>Created</span>
<span>{ tablo.CreatedAt.Time.Format("Jan 2, 2006") }</span>
</div>
@ui.Badge(ui.BadgeProps{Label: "In progress", Variant: ui.BadgeVariantPrimary})
<div class="project-progress-track">
<div class="project-progress-bar" style="width: 0%;"></div>
<div class="tablo-metadata-row">
<span class="tablo-meta-segment">
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><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>
<span>Created</span>
<span>{ tablo.CreatedAt.Time.Format("Jan 2, 2006") }</span>
</span>
<span class="tablo-meta-segment">
@ui.Badge(ui.BadgeProps{Label: "In progress", Variant: ui.BadgeVariantPrimary})
</span>
<span class="tablo-meta-segment tablo-meta-progress">
<div class="project-progress-track" style="min-width:120px">
<div class="tablo-progress-bar" style="width:0%"></div>
</div>
<strong>0%</strong>
</span>
</div>
</header>
<nav class="tablo-tab-bar">
<a
href={ templ.SafeURL("/tablos/" + tablo.ID.String()) }
hx-get={ "/tablos/" + tablo.ID.String() }
hx-target="#tab-content"
hx-swap="innerHTML"
hx-push-url={ "/tablos/" + tablo.ID.String() }
if activeTab == "overview" || activeTab == "" {
class="tab-nav-item is-active"
} else {
class="tab-nav-item"
}
>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="7" height="9" x="3" y="3" rx="1"></rect><rect width="7" height="5" x="14" y="3" rx="1"></rect><rect width="7" height="9" x="14" y="12" rx="1"></rect><rect width="7" height="5" x="3" y="16" rx="1"></rect></svg>
Vue d&apos;ensemble
</a>
<a
href={ templ.SafeURL("/tablos/" + tablo.ID.String() + "/tasks") }
hx-get={ "/tablos/" + tablo.ID.String() + "/tasks" }
hx-target="#tab-content"
hx-swap="innerHTML"
hx-push-url={ "/tablos/" + tablo.ID.String() + "/tasks" }
if activeTab == "tasks" {
class="tab-nav-item is-active"
} else {
class="tab-nav-item"
}
>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m3 17 2 2 4-4"></path><path d="m3 7 2 2 4-4"></path><path d="M13 6h8"></path><path d="M13 12h8"></path><path d="M13 18h8"></path></svg>
Tâches
</a>
<a
href={ templ.SafeURL("/tablos/" + tablo.ID.String() + "/files") }
hx-get={ "/tablos/" + tablo.ID.String() + "/files" }
hx-target="#tab-content"
hx-swap="innerHTML"
hx-push-url={ "/tablos/" + tablo.ID.String() + "/files" }
if activeTab == "files" {
class="tab-nav-item is-active"
} else {
class="tab-nav-item"
}
>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 20h16a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.93a2 2 0 0 1-1.66-.9l-.82-1.2A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13c0 1.1.9 2 2 2Z"></path></svg>
Fichiers
</a>
<a
href={ templ.SafeURL("/tablos/" + tablo.ID.String() + "/discussion") }
hx-get={ "/tablos/" + tablo.ID.String() + "/discussion" }
hx-target="#tab-content"
hx-swap="innerHTML"
hx-push-url={ "/tablos/" + tablo.ID.String() + "/discussion" }
if activeTab == "discussion" {
class="tab-nav-item is-active"
} else {
class="tab-nav-item"
}
>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M7.9 20A9 9 0 1 0 4 16.1L2 22Z"></path></svg>
Discussion
</a>
<a
href={ templ.SafeURL("/tablos/" + tablo.ID.String() + "/events") }
hx-get={ "/tablos/" + tablo.ID.String() + "/events" }
hx-target="#tab-content"
hx-swap="innerHTML"
hx-push-url={ "/tablos/" + tablo.ID.String() + "/events" }
if activeTab == "events" {
class="tab-nav-item is-active"
} else {
class="tab-nav-item"
}
>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><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>
Événements
</a>
</nav>
<!-- Tab content area — HTMX tab switches target this div -->
<div id="tab-content" class="pt-6 pb-8">
if activeTab == "tasks" {
@TasksTabFragment(tablo, tasks, etapes, counts, filter, csrfToken)
} else if activeTab == "files" {
@FilesTabFragment(tablo, files, csrfToken)
} else if activeTab == "events" {
@EventsTabFragment(tablo, events, csrfToken)
} else if activeTab == "discussion" {
@DiscussionTabFragment(tablo, discussion, DiscussionForm{}, DiscussionErrors{}, csrfToken)
} else {
@TabloOverviewTabFragment(tablo, csrfToken)
}
</div>
</div>
<!-- Sticky tab navigation (D-07, D-08) -->
<div class="w-full bg-white dark:bg-background sticky top-0 z-40">
<div class="py-2 px-4">
<nav class="tab-nav">
<a
href={ templ.SafeURL("/tablos/" + tablo.ID.String()) }
hx-get={ "/tablos/" + tablo.ID.String() }
hx-target="#tab-content"
hx-swap="innerHTML"
hx-push-url={ "/tablos/" + tablo.ID.String() }
if activeTab == "overview" || activeTab == "" {
class="tab-nav-item is-active"
} else {
class="tab-nav-item"
}
>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="7" height="9" x="3" y="3" rx="1"></rect><rect width="7" height="5" x="14" y="3" rx="1"></rect><rect width="7" height="9" x="14" y="12" rx="1"></rect><rect width="7" height="5" x="3" y="16" rx="1"></rect></svg>
Overview
</a>
<a
href={ templ.SafeURL("/tablos/" + tablo.ID.String() + "/tasks") }
hx-get={ "/tablos/" + tablo.ID.String() + "/tasks" }
hx-target="#tab-content"
hx-swap="innerHTML"
hx-push-url={ "/tablos/" + tablo.ID.String() + "/tasks" }
if activeTab == "tasks" {
class="tab-nav-item is-active"
} else {
class="tab-nav-item"
}
>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m3 17 2 2 4-4"></path><path d="m3 7 2 2 4-4"></path><path d="M13 6h8"></path><path d="M13 12h8"></path><path d="M13 18h8"></path></svg>
Tasks
</a>
<a
href={ templ.SafeURL("/tablos/" + tablo.ID.String() + "/files") }
hx-get={ "/tablos/" + tablo.ID.String() + "/files" }
hx-target="#tab-content"
hx-swap="innerHTML"
hx-push-url={ "/tablos/" + tablo.ID.String() + "/files" }
if activeTab == "files" {
class="tab-nav-item is-active"
} else {
class="tab-nav-item"
}
>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 20h16a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.93a2 2 0 0 1-1.66-.9l-.82-1.2A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13c0 1.1.9 2 2 2Z"></path></svg>
Files
</a>
<a
href={ templ.SafeURL("/tablos/" + tablo.ID.String() + "/discussion") }
hx-get={ "/tablos/" + tablo.ID.String() + "/discussion" }
hx-target="#tab-content"
hx-swap="innerHTML"
hx-push-url={ "/tablos/" + tablo.ID.String() + "/discussion" }
if activeTab == "discussion" {
class="tab-nav-item is-active"
} else {
class="tab-nav-item"
}
>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M7.9 20A9 9 0 1 0 4 16.1L2 22Z"></path></svg>
Discussion
</a>
<a
href={ templ.SafeURL("/tablos/" + tablo.ID.String() + "/events") }
hx-get={ "/tablos/" + tablo.ID.String() + "/events" }
hx-target="#tab-content"
hx-swap="innerHTML"
hx-push-url={ "/tablos/" + tablo.ID.String() + "/events" }
if activeTab == "events" {
class="tab-nav-item is-active"
} else {
class="tab-nav-item"
}
>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><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>
Events
</a>
</nav>
</div>
</div>
<!-- Tab content area — HTMX tab switches target this div -->
<div id="tab-content" class="px-4 sm:px-6 pt-6 pb-8">
if activeTab == "tasks" {
@TasksTabFragment(tablo, tasks, etapes, counts, filter, csrfToken)
} else if activeTab == "files" {
@FilesTabFragment(tablo, files, csrfToken)
} else if activeTab == "events" {
@EventsTabFragment(tablo, events, csrfToken)
} else if activeTab == "discussion" {
@DiscussionTabFragment(tablo, discussion, DiscussionForm{}, DiscussionErrors{}, csrfToken)
} else {
@TabloOverviewTabFragment(tablo, csrfToken)
}
</div>
}
}

View file

@ -88,7 +88,7 @@ templ EtapeGroupHeader(group EtapeGroup) {
// UI-SPEC §1 and D-08.
templ KanbanBoard(tabloID uuid.UUID, csrfToken string, tasks []sqlc.Task, filter EtapeFilter, etapes []sqlc.Etape) {
{{ grouped := groupTasksByStatus(tasks) }}
<div id="kanban-board" class="flex gap-4 overflow-x-auto pb-4">
<div id="kanban-board" class="tablo-kanban-board">
<form
id="reorder-form"
method="POST"
@ -171,37 +171,31 @@ templ KanbanBoard(tabloID uuid.UUID, csrfToken string, tasks []sqlc.Task, filter
// Tasks are grouped by etape in declaration order; unassigned tasks appear last.
// UI-SPEC §1 and §2.
templ KanbanColumn(tabloID uuid.UUID, status sqlc.TaskStatus, tasks []sqlc.Task, csrfToken string, filter EtapeFilter, etapes []sqlc.Etape) {
<div class="kanban-column">
<div class="tasks-section">
<div class="tasks-section-header">
<div style="display: flex; align-items: center; gap: 0.5rem;">
<h3>{ TaskColumnLabels[status] }</h3>
<span id={ "task-count-badge-" + string(status) }>
@ui.Badge(ui.BadgeProps{Label: strconv.Itoa(len(tasks)), Variant: ui.BadgeVariantInfo})
</span>
</div>
<div id={ "add-task-slot-" + string(status) }>
@AddTaskTrigger(tabloID, status, csrfToken, filter)
</div>
<div class="tablo-kanban-column" data-status={ string(status) }>
<div class="tablo-kanban-column-header">
<span class="tablo-kanban-column-title">{ TaskColumnLabels[status] }</span>
<span id={ "task-count-badge-" + string(status) } class="tablo-kanban-task-count">{ strconv.Itoa(len(tasks)) }</span>
<div id={ "add-task-slot-" + string(status) }>
@AddTaskTrigger(tabloID, status, csrfToken, filter)
</div>
<div
class="task-list sortable-column"
data-status={ string(status) }
id={ "column-" + string(status) }
aria-label={ TaskColumnLabels[status] + " column" }
>
if len(tasks) == 0 {
<p class="task-list-empty">No tasks yet</p>
} else {
{{ groups := groupTasksByEtape(tasks, etapes) }}
for _, group := range groups {
@EtapeGroupHeader(group)
for _, task := range group.Tasks {
@TaskCard(tabloID, task, csrfToken)
}
</div>
<div
class="task-list sortable-column"
data-status={ string(status) }
id={ "column-" + string(status) }
aria-label={ TaskColumnLabels[status] + " column" }
>
if len(tasks) == 0 {
<p class="tablo-kanban-empty">Aucune tâche</p>
} else {
{{ groups := groupTasksByEtape(tasks, etapes) }}
for _, group := range groups {
@EtapeGroupHeader(group)
for _, task := range group.Tasks {
@TaskCard(tabloID, task, csrfToken)
}
}
</div>
}
</div>
</div>
}
@ -212,7 +206,7 @@ templ KanbanColumn(tabloID uuid.UUID, status sqlc.TaskStatus, tasks []sqlc.Task,
templ TaskCard(tabloID uuid.UUID, task sqlc.Task, csrfToken string) {
<div class="task-card-zone" id={ "task-" + task.ID.String() }>
<div
class="task-row task-card"
class="task-card"
data-task-id={ task.ID.String() }
hx-get={ "/tablos/" + tabloID.String() + "/tasks/" + task.ID.String() + "/edit" }
hx-target="closest .task-card-zone"
@ -220,23 +214,23 @@ templ TaskCard(tabloID uuid.UUID, task sqlc.Task, csrfToken string) {
role="button"
aria-label={ "Edit task: " + task.Title }
>
<div class="task-drag-handle" aria-hidden="true">⠿</div>
<div class="task-check" role="checkbox" aria-checked="false"></div>
<div class="task-body">
<p>{ task.Title }</p>
<div class="task-card-top-row">
<span class="task-drag-handle" aria-hidden="true">⠿</span>
<span class="task-card-title">{ task.Title }</span>
@ui.IconButton(ui.IconButtonProps{
Label: "Delete task: " + task.Title,
Icon: "trash",
Variant: ui.IconButtonVariantDanger,
Tone: ui.IconButtonToneGhost,
Type: "button",
Attrs: templ.Attributes{
"class": "task-card-delete",
"hx-get": "/tablos/" + tabloID.String() + "/tasks/" + task.ID.String() + "/delete-confirm",
"hx-target": "closest .task-card-zone",
"hx-swap": "outerHTML",
},
})
</div>
@ui.IconButton(ui.IconButtonProps{
Label: "Delete task: " + task.Title,
Icon: "trash",
Variant: ui.IconButtonVariantDanger,
Tone: ui.IconButtonToneGhost,
Type: "button",
Attrs: templ.Attributes{
"hx-get": "/tablos/" + tabloID.String() + "/tasks/" + task.ID.String() + "/delete-confirm",
"hx-target": "closest .task-card-zone",
"hx-swap": "outerHTML",
},
})
</div>
</div>
}