feat(16-02): restyle TabloDetailPage header, tab nav, metadata row; remove EtapeStrip

- Replace header with project-card-top layout: color avatar with first char, tablo-title-zone
- Replace Discussion link/Invite button/Delete button with @ui.IconButton and @ui.Button using design token variants
- Add inline tablo-delete-zone with trash @ui.IconButton (does not use TabloDeleteButtonFragment)
- Replace metadata row hardcoded flex/hex classes with tablo-metadata-row, @ui.Badge(BadgeVariantPrimary), project-progress-track/bar
- Replace 5 tab nav <a> elements from long inline Tailwind hex classes to tab-nav-item / tab-nav-item is-active
- Wrap tab nav in class="tab-nav" replacing raw flex container
- Move @TabloDescDisplay call from persistent header into TabloOverviewTabFragment
- Remove @EtapeStrip call from TasksTabFragment (D-E01; KanbanBoard call site update deferred to Plan 03)
- Remove last #804EEC hex value from TabloTitleDisplay hover class
- Regenerated tablos_templ.go via templ generate
This commit is contained in:
Arthur Belleville 2026-05-16 23:37:16 +02:00
parent 4e0336c950
commit 443a38dfc8
No known key found for this signature in database

View file

@ -240,63 +240,83 @@ templ TabloCardWithOOBFormClear(tablo sqlc.Tablo, csrfToken string) {
// 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, 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) { 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) {
@AppLayout("Tablos — Xtablo", user, csrfToken, activePath, sidebarTablos) { @AppLayout("Tablos — Xtablo", user, csrfToken, activePath, sidebarTablos) {
<!-- Header: title row + action buttons --> <!-- Header: project-card-top layout with color avatar, title zone, and action controls -->
<div class="px-4 pt-4"> <div class="px-4 pt-4">
<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="project-card-top">
<div class="flex items-center gap-4"> <div class="project-card-title-row">
if tablo.Color.Valid && tablo.Color.String != "" {
<span class="project-avatar" style={ "background-color: " + tablo.Color.String }>
if len(tablo.Title) > 0 {
{ string([]rune(tablo.Title)[0:1]) }
}
</span>
} else {
<span class="project-avatar">
if len(tablo.Title) > 0 {
{ string([]rune(tablo.Title)[0:1]) }
}
</span>
}
<div class="tablo-title-zone"> <div class="tablo-title-zone">
@TabloTitleDisplay(tablo, csrfToken) @TabloTitleDisplay(tablo, csrfToken)
</div> </div>
</div> </div>
<div class="flex items-center gap-3 w-full sm:w-auto"> <div class="flex items-center gap-3">
<a <a
href={ templ.SafeURL("/tablos/" + tablo.ID.String() + "/discussion") } href={ templ.SafeURL("/tablos/" + tablo.ID.String() + "/discussion") }
hx-get={ "/tablos/" + tablo.ID.String() + "/discussion" } hx-get={ "/tablos/" + tablo.ID.String() + "/discussion" }
hx-target="#tab-content" hx-target="#tab-content"
hx-swap="innerHTML" hx-swap="innerHTML"
hx-push-url={ "/tablos/" + tablo.ID.String() + "/discussion" } 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> @ui.IconButton(ui.IconButtonProps{
Discussion Label: "Discussion",
Icon: "chat",
Variant: ui.IconButtonVariantNeutral,
Tone: ui.IconButtonToneGhost,
Type: "button",
})
</a> </a>
<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"> @ui.Button(ui.ButtonProps{
<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> Label: "Invite Member",
Invite Variant: ui.ButtonVariantDefault,
</button> Tone: ui.ButtonToneSoft,
Size: ui.SizeMD,
Type: "button",
})
<div class="tablo-delete-zone"> <div class="tablo-delete-zone">
@TabloDeleteButtonFragment(tablo, csrfToken) @ui.IconButton(ui.IconButtonProps{
Label: "Delete tablo",
Icon: "trash",
Variant: ui.IconButtonVariantDanger,
Tone: ui.IconButtonToneGhost,
Type: "button",
Attrs: templ.Attributes{
"hx-get": "/tablos/" + tablo.ID.String() + "/delete-confirm",
"hx-target": "closest .tablo-delete-zone",
"hx-swap": "outerHTML",
},
})
</div> </div>
</div> </div>
</div> </div>
<!-- Metadata row: created date, status, progress --> <!-- Metadata row: created date, status, 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="tablo-metadata-row">
<div class="flex items-center gap-2 sm:border-r sm:border-[#F2F4F7] sm:dark:border-gray-700 sm:pr-6"> <div class="tablo-metadata-date">
<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> <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 class="text-[#667085]">Created</span> <span>Created</span>
<span class="text-foreground font-medium">{ tablo.CreatedAt.Time.Format("Jan 2, 2006") }</span> <span>{ tablo.CreatedAt.Time.Format("Jan 2, 2006") }</span>
</div> </div>
<div class="flex items-center gap-2 sm:border-r sm:border-[#F2F4F7] sm:dark:border-gray-700 sm:pr-6"> @ui.Badge(ui.BadgeProps{Label: "In progress", Variant: ui.BadgeVariantPrimary})
<span class="text-[#667085]">Status</span> <div class="project-progress-track">
<span class="px-3 py-1 rounded-full text-xs font-medium bg-yellow-50 text-yellow-700 border border-yellow-200 dark:bg-yellow-950/30 dark:text-yellow-400 dark:border-yellow-800">In progress</span> <div class="project-progress-bar" style="width: 0%;"></div>
</div> </div>
<div class="flex items-center gap-2">
<span class="text-[#667085]">Progress</span>
<div class="relative w-24 h-2 bg-gray-200 dark:bg-gray-700 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>
<!-- Description inline-edit zone -->
<div class="tablo-desc-zone mb-4">
@TabloDescDisplay(tablo, csrfToken)
</div> </div>
</div> </div>
<!-- Sticky tab navigation (D-07, D-08) --> <!-- Sticky tab navigation (D-07, D-08) -->
<div class="w-full bg-white dark:bg-background sticky top-0 z-40"> <div class="w-full bg-white dark:bg-background sticky top-0 z-40">
<div class="py-2"> <div class="py-2 px-4">
<nav class="flex items-center gap-4 sm:gap-6 border-b border-[#F2F4F7] dark:border-gray-700 overflow-x-auto -mx-4 px-4" style="scrollbar-width: none;"> <nav class="tab-nav">
<a <a
href={ templ.SafeURL("/tablos/" + tablo.ID.String()) } href={ templ.SafeURL("/tablos/" + tablo.ID.String()) }
hx-get={ "/tablos/" + tablo.ID.String() } hx-get={ "/tablos/" + tablo.ID.String() }
@ -304,9 +324,9 @@ templ TabloDetailPage(user *auth.User, csrfToken string, activePath string, side
hx-swap="innerHTML" hx-swap="innerHTML"
hx-push-url={ "/tablos/" + tablo.ID.String() } hx-push-url={ "/tablos/" + tablo.ID.String() }
if activeTab == "overview" || activeTab == "" { 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]" class="tab-nav-item is-active"
} else { } 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" 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> <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>
@ -319,9 +339,9 @@ templ TabloDetailPage(user *auth.User, csrfToken string, activePath string, side
hx-swap="innerHTML" hx-swap="innerHTML"
hx-push-url={ "/tablos/" + tablo.ID.String() + "/tasks" } hx-push-url={ "/tablos/" + tablo.ID.String() + "/tasks" }
if activeTab == "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]" class="tab-nav-item is-active"
} else { } 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" 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> <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>
@ -334,9 +354,9 @@ templ TabloDetailPage(user *auth.User, csrfToken string, activePath string, side
hx-swap="innerHTML" hx-swap="innerHTML"
hx-push-url={ "/tablos/" + tablo.ID.String() + "/files" } hx-push-url={ "/tablos/" + tablo.ID.String() + "/files" }
if activeTab == "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]" class="tab-nav-item is-active"
} else { } 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" 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> <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>
@ -349,9 +369,9 @@ templ TabloDetailPage(user *auth.User, csrfToken string, activePath string, side
hx-swap="innerHTML" hx-swap="innerHTML"
hx-push-url={ "/tablos/" + tablo.ID.String() + "/discussion" } hx-push-url={ "/tablos/" + tablo.ID.String() + "/discussion" }
if activeTab == "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]" class="tab-nav-item is-active"
} else { } 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" 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> <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>
@ -364,9 +384,9 @@ templ TabloDetailPage(user *auth.User, csrfToken string, activePath string, side
hx-swap="innerHTML" hx-swap="innerHTML"
hx-push-url={ "/tablos/" + tablo.ID.String() + "/events" } hx-push-url={ "/tablos/" + tablo.ID.String() + "/events" }
if activeTab == "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]" class="tab-nav-item is-active"
} else { } 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" 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> <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>
@ -392,15 +412,14 @@ templ TabloDetailPage(user *auth.User, csrfToken string, activePath string, side
} }
} }
// TabloOverviewTabFragment renders the overview tab content — description display. // TabloOverviewTabFragment renders the overview tab content — description inline-edit zone.
// Returned as a standalone fragment for HTMX tab-switch responses. // Returned as a standalone fragment for HTMX tab-switch responses.
// Description zone relocated here from the persistent header (Phase 16 Plan 02).
templ TabloOverviewTabFragment(tablo sqlc.Tablo, csrfToken string) { templ TabloOverviewTabFragment(tablo sqlc.Tablo, csrfToken string) {
<div class="overview-tab"> <div class="overview-tab">
if tablo.Description.Valid && tablo.Description.String != "" { <div class="tablo-desc-zone">
<p class="text-base text-slate-600">{ tablo.Description.String }</p> @TabloDescDisplay(tablo, csrfToken)
} else { </div>
<p class="text-sm text-slate-400 italic">No description.</p>
}
</div> </div>
} }
@ -409,7 +428,6 @@ templ TabloOverviewTabFragment(tablo sqlc.Tablo, csrfToken string) {
// Lives in tablos.templ (tablo-level concern) per plan D-07. // Lives in tablos.templ (tablo-level concern) per plan D-07.
templ TasksTabFragment(tablo sqlc.Tablo, tasks []sqlc.Task, etapes []sqlc.Etape, counts EtapeTaskCounts, filter EtapeFilter, csrfToken string) { templ TasksTabFragment(tablo sqlc.Tablo, tasks []sqlc.Task, etapes []sqlc.Etape, counts EtapeTaskCounts, filter EtapeFilter, csrfToken string) {
<div id="tasks-tab"> <div id="tasks-tab">
@EtapeStrip(tablo.ID, etapes, counts, filter, csrfToken, false)
@KanbanBoard(tablo.ID, csrfToken, tasks, filter) @KanbanBoard(tablo.ID, csrfToken, tasks, filter)
</div> </div>
} }
@ -420,7 +438,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 md:text-3xl font-bold text-foreground cursor-pointer hover:text-[#804EEC] transition-colors" class="tablo-title-zone text-xl md:text-3xl font-bold text-foreground cursor-pointer transition-colors"
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"