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:
parent
efa9a85dd7
commit
63e7d65290
3 changed files with 347 additions and 187 deletions
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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'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>
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue