feat(15-03): restyle tablo detail page header and tab nav to match reference design
- Replace plain back-link + title with structured header: title row + metadata row - Title uses larger md:text-3xl font-bold style with inline-edit preserved - Add Discussion and Invite action buttons in header (purple brand colors) - Add metadata row: created date, hardcoded En cours status badge, 0% progress bar - Replace plain slate tab nav with purple-accent tab bar (icon + label per tab) - Tab active state: text-[#804EEC] border-[#804EEC]; inactive: text-[#667085] - Tab bar is sticky (top-0 z-40) with horizontal scroll and hidden scrollbar - Keep all HTMX attributes, hx-push-url, hx-target="#tab-content" logic unchanged - tablo-title-zone, tablo-desc-zone, tablo-delete-zone elements retained
This commit is contained in:
parent
ae0ab0ca5b
commit
6953536dd8
1 changed files with 149 additions and 78 deletions
|
|
@ -179,90 +179,161 @@ templ TabloCardWithOOBFormClear(tablo sqlc.Tablo, csrfToken string) {
|
||||||
<div id="create-form-slot" hx-swap-oob="true"></div>
|
<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.
|
// TabloDetailPage renders the full detail page for a single tablo with a tab layout.
|
||||||
// Tabs: Overview / Tasks / Files. activeTab selects the initially rendered tab content.
|
// Tabs: Overview / Tasks / Files / Events / Discussion. activeTab selects the initially rendered tab content.
|
||||||
// files and tasks are pre-fetched slices for the active tab (may be nil for inactive tabs).
|
// files and tasks are pre-fetched slices for the active tab (may be nil for inactive tabs).
|
||||||
// UI-SPEC §3 Interaction Contract — GET /tablos/{id}.
|
// UI-SPEC §3 Interaction Contract — GET /tablos/{id}.
|
||||||
// D-07: signature includes activeTab string param; D-08: tab bar links carry hx-push-url.
|
// D-07: signature includes activeTab string param; D-08: tab bar links carry hx-push-url.
|
||||||
templ TabloDetailPage(user *auth.User, csrfToken string, tablo sqlc.Tablo, tasks []sqlc.Task, etapes []sqlc.Etape, counts EtapeTaskCounts, filter EtapeFilter, files []sqlc.TabloFile, events EventsCalendar, discussion DiscussionTabData, activeTab string) {
|
templ TabloDetailPage(user *auth.User, csrfToken string, tablo sqlc.Tablo, tasks []sqlc.Task, etapes []sqlc.Etape, counts EtapeTaskCounts, filter EtapeFilter, files []sqlc.TabloFile, events EventsCalendar, discussion DiscussionTabData, activeTab string) {
|
||||||
@Layout("Tablos — Xtablo", user, csrfToken) {
|
@Layout("Tablos — Xtablo", user, csrfToken) {
|
||||||
<div class="mb-4">
|
<!-- Header section: title row + metadata row -->
|
||||||
<a href="/" class="text-sm text-slate-600 hover:underline">← Back to tablos</a>
|
<div class="px-0 pt-0">
|
||||||
|
<!-- Title row: h1 + action buttons -->
|
||||||
|
<div class="flex flex-col md:flex-row items-start justify-between mb-6 border-b border-[#F2F4F7] dark:border-gray-700 pb-5 gap-5 md:gap-0">
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<!-- tablo-title-zone: inline-edit display + form for title (UI-SPEC §4) -->
|
||||||
|
@TabloTitleDisplay(tablo, csrfToken)
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-3 w-full sm:w-auto">
|
||||||
|
<!-- Discussion action button -->
|
||||||
|
<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" }
|
||||||
|
class="bg-[#804EEC] hover:bg-[#6f3fd4] text-white font-medium py-2.5 px-4 rounded-lg flex items-center justify-center gap-2 transition-colors flex-1 sm:flex-none min-h-[44px] text-sm"
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
<!-- Invite button (placeholder) -->
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="border border-[#804EEC] text-[#804EEC] hover:bg-[#804EEC]/10 font-medium py-2.5 px-4 rounded-lg flex items-center justify-center gap-2 transition-colors flex-1 sm:flex-none min-h-[44px] text-sm"
|
||||||
|
>
|
||||||
|
<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="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><line x1="19" x2="19" y1="8" y2="14"></line><line x1="22" x2="16" y1="11" y2="11"></line></svg>
|
||||||
|
Invite
|
||||||
|
</button>
|
||||||
|
<!-- tablo-delete-zone: delete button (UI-SPEC §3) -->
|
||||||
|
<div class="tablo-delete-zone">
|
||||||
|
@TabloDeleteButtonFragment(tablo, csrfToken)
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Metadata row: created date, status badge, progress -->
|
||||||
|
<div class="flex flex-wrap items-center gap-3 sm:gap-6 text-sm border-b border-[#F2F4F7] dark:border-gray-700 pb-4 mb-4">
|
||||||
|
<div class="flex items-center gap-2 sm:border-r sm:border-[#F2F4F7] sm:dark:border-gray-700 sm:pr-6">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-[#667085]"><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 class="text-[#667085]">Créé le</span>
|
||||||
|
<span class="text-foreground font-medium">{ tablo.CreatedAt.Time.Format("Jan 2, 2006") }</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2 sm:border-r sm:border-[#F2F4F7] sm:dark:border-gray-700 sm:pr-6">
|
||||||
|
<span class="text-[#667085]">Statut</span>
|
||||||
|
<span class="px-3 py-1 rounded-full text-xs font-medium bg-yellow-50 text-yellow-700 border border-yellow-200">En cours</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="text-[#667085]">Progression</span>
|
||||||
|
<div class="relative w-24 h-2 bg-gray-200 rounded-full overflow-hidden">
|
||||||
|
<div class="absolute inset-y-0 left-0 bg-green-500" style="width: 0%;"></div>
|
||||||
|
</div>
|
||||||
|
<span class="text-foreground font-medium">0%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- tablo-desc-zone: inline-edit display + form for description (UI-SPEC §4) -->
|
||||||
|
<div class="tablo-desc-zone mb-2">
|
||||||
|
@TabloDescDisplay(tablo, csrfToken)
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="tablo-title-zone">
|
<!-- Sticky tab navigation bar (D-07, D-08) -->
|
||||||
@TabloTitleDisplay(tablo, csrfToken)
|
<div class="w-full bg-white dark:bg-background sticky top-0 z-40">
|
||||||
|
<div class="py-2">
|
||||||
|
<nav class="flex items-center gap-4 sm:gap-6 mb-4 border-b border-[#F2F4F7] dark:border-gray-700 overflow-x-auto -mx-4 px-4" style="scrollbar-width: none;">
|
||||||
|
<!-- Overview tab -->
|
||||||
|
<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="flex items-center gap-2 pb-3 px-1 text-sm font-semibold transition-colors border-b-2 shrink-0 min-h-[44px] text-[#804EEC] border-[#804EEC]"
|
||||||
|
} else {
|
||||||
|
class="flex items-center gap-2 pb-3 px-1 text-sm font-semibold transition-colors border-b-2 border-transparent shrink-0 min-h-[44px] text-[#667085] hover:text-gray-900"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
<!-- Tasks tab -->
|
||||||
|
<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="flex items-center gap-2 pb-3 px-1 text-sm font-semibold transition-colors border-b-2 shrink-0 min-h-[44px] text-[#804EEC] border-[#804EEC]"
|
||||||
|
} else {
|
||||||
|
class="flex items-center gap-2 pb-3 px-1 text-sm font-semibold transition-colors border-b-2 border-transparent shrink-0 min-h-[44px] text-[#667085] hover:text-gray-900"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<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="3" rx="2"></rect><path d="m9 12 2 2 4-4"></path></svg>
|
||||||
|
Étapes
|
||||||
|
</a>
|
||||||
|
<!-- Files tab -->
|
||||||
|
<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="flex items-center gap-2 pb-3 px-1 text-sm font-semibold transition-colors border-b-2 shrink-0 min-h-[44px] text-[#804EEC] border-[#804EEC]"
|
||||||
|
} else {
|
||||||
|
class="flex items-center gap-2 pb-3 px-1 text-sm font-semibold transition-colors border-b-2 border-transparent shrink-0 min-h-[44px] text-[#667085] hover:text-gray-900"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
<!-- Discussion tab -->
|
||||||
|
<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="flex items-center gap-2 pb-3 px-1 text-sm font-semibold transition-colors border-b-2 shrink-0 min-h-[44px] text-[#804EEC] border-[#804EEC]"
|
||||||
|
} else {
|
||||||
|
class="flex items-center gap-2 pb-3 px-1 text-sm font-semibold transition-colors border-b-2 border-transparent shrink-0 min-h-[44px] text-[#667085] hover:text-gray-900"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
<!-- Events tab -->
|
||||||
|
<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="flex items-center gap-2 pb-3 px-1 text-sm font-semibold transition-colors border-b-2 shrink-0 min-h-[44px] text-[#804EEC] border-[#804EEC]"
|
||||||
|
} else {
|
||||||
|
class="flex items-center gap-2 pb-3 px-1 text-sm font-semibold transition-colors border-b-2 border-transparent shrink-0 min-h-[44px] text-[#667085] hover:text-gray-900"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<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>
|
</div>
|
||||||
<div class="tablo-desc-zone">
|
|
||||||
@TabloDescDisplay(tablo, csrfToken)
|
|
||||||
</div>
|
|
||||||
<div class="tablo-delete-zone">
|
|
||||||
@TabloDeleteButtonFragment(tablo, csrfToken)
|
|
||||||
</div>
|
|
||||||
<!-- Tab navigation bar (D-07, D-08) -->
|
|
||||||
<nav class="mt-8 flex gap-1 border-b border-slate-200">
|
|
||||||
<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="px-4 py-2 text-sm font-semibold border-b-2 border-slate-800 text-slate-800 -mb-px"
|
|
||||||
} else {
|
|
||||||
class="px-4 py-2 text-sm text-slate-600 hover:text-slate-800 hover:border-b-2 hover:border-slate-400 -mb-px"
|
|
||||||
}
|
|
||||||
>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="px-4 py-2 text-sm font-semibold border-b-2 border-slate-800 text-slate-800 -mb-px"
|
|
||||||
} else {
|
|
||||||
class="px-4 py-2 text-sm text-slate-600 hover:text-slate-800 hover:border-b-2 hover:border-slate-400 -mb-px"
|
|
||||||
}
|
|
||||||
>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="px-4 py-2 text-sm font-semibold border-b-2 border-slate-800 text-slate-800 -mb-px"
|
|
||||||
} else {
|
|
||||||
class="px-4 py-2 text-sm text-slate-600 hover:text-slate-800 hover:border-b-2 hover:border-slate-400 -mb-px"
|
|
||||||
}
|
|
||||||
>Files</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="px-4 py-2 text-sm font-semibold border-b-2 border-slate-800 text-slate-800 -mb-px"
|
|
||||||
} else {
|
|
||||||
class="px-4 py-2 text-sm text-slate-600 hover:text-slate-800 hover:border-b-2 hover:border-slate-400 -mb-px"
|
|
||||||
}
|
|
||||||
>Events</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="px-4 py-2 text-sm font-semibold border-b-2 border-slate-800 text-slate-800 -mb-px"
|
|
||||||
} else {
|
|
||||||
class="px-4 py-2 text-sm text-slate-600 hover:text-slate-800 hover:border-b-2 hover:border-slate-400 -mb-px"
|
|
||||||
}
|
|
||||||
>Discussion</a>
|
|
||||||
</nav>
|
|
||||||
<!-- Tab content area — HTMX tab switches target this div -->
|
<!-- Tab content area — HTMX tab switches target this div -->
|
||||||
<div id="tab-content" class="mt-6">
|
<div id="tab-content" class="px-0 sm:px-0 pt-6 pb-8">
|
||||||
if activeTab == "tasks" {
|
if activeTab == "tasks" {
|
||||||
@TasksTabFragment(tablo, tasks, etapes, counts, filter, csrfToken)
|
@TasksTabFragment(tablo, tasks, etapes, counts, filter, csrfToken)
|
||||||
} else if activeTab == "files" {
|
} else if activeTab == "files" {
|
||||||
|
|
@ -306,7 +377,7 @@ templ TasksTabFragment(tablo sqlc.Tablo, tasks []sqlc.Task, etapes []sqlc.Etape,
|
||||||
// UI-SPEC §4 Interaction Contract — title inline-edit display state.
|
// UI-SPEC §4 Interaction Contract — title inline-edit display state.
|
||||||
templ TabloTitleDisplay(tablo sqlc.Tablo, csrfToken string) {
|
templ TabloTitleDisplay(tablo sqlc.Tablo, csrfToken string) {
|
||||||
<h1
|
<h1
|
||||||
class="tablo-title-zone text-xl font-semibold leading-snug cursor-pointer hover:text-slate-600"
|
class="tablo-title-zone text-xl md:text-3xl font-bold text-foreground cursor-pointer hover:text-slate-600"
|
||||||
hx-get={ "/tablos/" + tablo.ID.String() + "/edit-title" }
|
hx-get={ "/tablos/" + tablo.ID.String() + "/edit-title" }
|
||||||
hx-target="closest .tablo-title-zone"
|
hx-target="closest .tablo-title-zone"
|
||||||
hx-swap="outerHTML"
|
hx-swap="outerHTML"
|
||||||
|
|
@ -340,7 +411,7 @@ templ TabloTitleEditFragment(tablo sqlc.Tablo, errs TabloUpdateErrors, csrfToken
|
||||||
name="title"
|
name="title"
|
||||||
value={ tablo.Title }
|
value={ tablo.Title }
|
||||||
required
|
required
|
||||||
class="mt-1 block w-full rounded border border-slate-300 px-3 py-2 text-sm placeholder-slate-400 focus:border-slate-500 focus:outline-none"
|
class="mt-1 block w-full rounded border border-slate-300 px-3 py-2 text-xl font-bold placeholder-slate-400 focus:border-[#804EEC] focus:outline-none"
|
||||||
/>
|
/>
|
||||||
@FieldError(errs.Title)
|
@FieldError(errs.Title)
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue