fix(15-03): apply tablo detail restyle with correct AppLayout signatures

Merge conflict resolution had taken the old template signatures (pre-AppLayout).
Restored HEAD~1 signatures then applied the header/tab-nav restyle:
- Title row: md:text-3xl font-bold, Discussion + Invite action buttons
- Metadata row: created date, status badge, 0% progress bar
- Sticky purple-accent tab bar (Overview, Tasks, Files, Discussion, Events)
- All HTMX wiring and AppLayout wrapping preserved
This commit is contained in:
Arthur Belleville 2026-05-16 22:24:52 +02:00
parent 79039721c6
commit 64ba8abbb0
No known key found for this signature in database

View file

@ -6,61 +6,114 @@ import (
"backend/internal/web/ui" "backend/internal/web/ui"
) )
// TablosDashboard renders the root authenticated dashboard: heading, "New tablo" // TablosDashboard renders the root authenticated dashboard with sidebar AppLayout.
// button, create-form slot, and the list of tablo cards (or empty state). // Shows a project-card grid (or empty state) for the user's tablos.
// UI-SPEC §1 Interaction Contract — GET /. // UI-SPEC §1 Interaction Contract — GET /.
templ TablosDashboard(user *auth.User, csrfToken string, tablos []TabloCardView) { templ TablosDashboard(user *auth.User, csrfToken string, activePath string, tablos []sqlc.Tablo, cards []TabloCardView) {
@Layout("Tablos — Xtablo", user, csrfToken) { @AppLayout("Tablos — Xtablo", user, csrfToken, activePath, tablos) {
<div class="flex items-center justify-between mb-6"> <section class="overview-section">
<h1 class="text-[28px] font-semibold leading-tight">Your Tablos</h1> <div class="overview-section-heading">
@ui.Button(ui.ButtonProps{ <h3>Your Tablos</h3>
Label: "New tablo", @ui.Button(ui.ButtonProps{
Variant: ui.ButtonVariantDefault, Label: "New tablo",
Tone: ui.ButtonToneSolid, Variant: ui.ButtonVariantDefault,
Size: ui.SizeMD, Tone: ui.ButtonToneSolid,
Type: "button", Size: ui.SizeMD,
Attrs: templ.Attributes{ Type: "button",
"hx-get": "/tablos/new", Attrs: templ.Attributes{
"hx-target": "#create-form-slot", "hx-get": "/tablos/new",
"hx-swap": "innerHTML", "hx-target": "#create-form-slot",
}, "hx-swap": "innerHTML",
}) },
</div> })
<div id="create-form-slot"></div> </div>
<div id="tablos-list"> <div id="create-form-slot"></div>
if len(tablos) == 0 { <div id="tablos-list" class="project-grid">
@TablosEmptyState() if len(cards) == 0 {
} else { @TablosEmptyState()
for _, tablo := range tablos { } else {
@TabloCard(tablo, csrfToken) for _, card := range cards {
@TabloProjectCard(card, csrfToken)
}
} }
} </div>
</div> </section>
} }
} }
// TablosEmptyState renders the empty-state copy when a user has no tablos. // TablosEmptyState renders the empty-state copy when a user has no tablos.
// Copy strings are locked by UI-SPEC Copywriting Contract. // Copy strings are locked by UI-SPEC Copywriting Contract.
// Uses ui.EmptyState for consistent styling across the app (Phase 13).
templ TablosEmptyState() { templ TablosEmptyState() {
<div class="text-center py-16"> @ui.EmptyState(ui.EmptyStateProps{
<h2 class="text-xl font-semibold leading-snug text-slate-800">No tablos yet</h2> Title: "No tablos yet",
<p class="mt-2 text-base text-slate-600">Create your first tablo to get started.</p> Description: "Create your first tablo to get started.",
<div class="mt-6"> Action: ui.Button(ui.ButtonProps{
@ui.Button(ui.ButtonProps{ Label: "New tablo",
Label: "New tablo", Variant: ui.ButtonVariantDefault,
Variant: ui.ButtonVariantDefault, Tone: ui.ButtonToneSolid,
Tone: ui.ButtonToneSolid, Size: ui.SizeMD,
Size: ui.SizeMD, Type: "button",
Type: "button", Attrs: templ.Attributes{
Attrs: templ.Attributes{ "hx-get": "/tablos/new",
"hx-get": "/tablos/new", "hx-target": "#create-form-slot",
"hx-target": "#create-form-slot", "hx-swap": "innerHTML",
"hx-swap": "innerHTML", },
"aria-label": "Create your first tablo", }),
}, })
}) }
// TabloProjectCard renders a single tablo as a project-card in the dashboard grid.
// Follows D-C02 design: colored avatar circle, title zone (with inline-edit support),
// creation date, and edit/delete icon buttons.
// Guards color rendering against null pgtype.Text values (Pitfall 6).
// Uses .Time accessor on pgtype.Timestamptz (Pitfall 6).
templ TabloProjectCard(card TabloCardView, csrfToken string) {
<article id={ "tablo-" + card.Tablo.ID.String() } class="project-card">
<div class="project-card-top">
<div class="flex items-center gap-2">
@ui.IconButton(ui.IconButtonProps{
Label: "Edit title",
Icon: "pencil",
Variant: ui.IconButtonVariantNeutral,
Tone: ui.IconButtonToneGhost,
Type: "button",
Attrs: templ.Attributes{
"hx-get": "/tablos/" + card.Tablo.ID.String() + "/edit-title",
"hx-target": "closest .tablo-title-zone",
"hx-swap": "outerHTML",
},
})
<div class="tablo-delete-zone">
@ui.IconButton(ui.IconButtonProps{
Label: "Delete tablo",
Icon: "trash",
Variant: ui.IconButtonVariantDanger,
Tone: ui.IconButtonToneGhost,
Type: "button",
Attrs: templ.Attributes{
"hx-get": "/tablos/" + card.Tablo.ID.String() + "/delete-confirm",
"hx-target": "closest .tablo-delete-zone",
"hx-swap": "outerHTML",
},
})
</div>
</div>
</div> </div>
</div> <div class="project-card-title-row">
if card.Tablo.Color.Valid && card.Tablo.Color.String != "" {
<span class="project-avatar" style={ "background-color: " + card.Tablo.Color.String }></span>
} else {
<span class="project-avatar"></span>
}
<div class="tablo-title-zone">
<h4>{ card.Tablo.Title }</h4>
</div>
</div>
<div class="project-date-row">
{ card.Tablo.CreatedAt.Time.Format("Jan 2, 2006") }
</div>
</article>
} }
// TabloCard renders a single tablo as a ui.Card on the dashboard. // TabloCard renders a single tablo as a ui.Card on the dashboard.
@ -179,23 +232,23 @@ 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 tab layout. // TabloDetailPage renders the full detail page for a single tablo with a 3-tab layout.
// Tabs: Overview / Tasks / Files / Events / Discussion. activeTab selects the initially rendered tab content. // Tabs: Overview / Tasks / Files. 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).
// activePath and sidebarTablos drive the AppLayout sidebar.
// 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, 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) {
@Layout("Tablos — Xtablo", user, csrfToken) { @AppLayout("Tablos — Xtablo", user, csrfToken, activePath, sidebarTablos) {
<!-- Header section: title row + metadata row --> <!-- Header: title row + action buttons -->
<div class="px-0 pt-0"> <div class="px-4 pt-4">
<!-- 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 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"> <div class="flex items-center gap-4">
<!-- tablo-title-zone: inline-edit display + form for title (UI-SPEC §4) --> <div class="tablo-title-zone">
@TabloTitleDisplay(tablo, csrfToken) @TabloTitleDisplay(tablo, csrfToken)
</div>
</div> </div>
<div class="flex items-center gap-3 w-full sm:w-auto"> <div class="flex items-center gap-3 w-full sm:w-auto">
<!-- Discussion action button -->
<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" }
@ -207,49 +260,43 @@ templ TabloDetailPage(user *auth.User, csrfToken string, tablo sqlc.Tablo, tasks
<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>
Discussion Discussion
</a> </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">
<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> <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 Invite
</button> </button>
<!-- tablo-delete-zone: delete button (UI-SPEC §3) -->
<div class="tablo-delete-zone"> <div class="tablo-delete-zone">
@TabloDeleteButtonFragment(tablo, csrfToken) @TabloDeleteButtonFragment(tablo, csrfToken)
</div> </div>
</div> </div>
</div> </div>
<!-- Metadata row: created date, status badge, 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="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"> <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> <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-[#667085]">Created</span>
<span class="text-foreground font-medium">{ tablo.CreatedAt.Time.Format("Jan 2, 2006") }</span> <span class="text-foreground font-medium">{ 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"> <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="text-[#667085]">Status</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> <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> </div>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<span class="text-[#667085]">Progression</span> <span class="text-[#667085]">Progress</span>
<div class="relative w-24 h-2 bg-gray-200 rounded-full overflow-hidden"> <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 class="absolute inset-y-0 left-0 bg-green-500" style="width: 0%;"></div>
</div> </div>
<span class="text-foreground font-medium">0%</span> <span class="text-foreground font-medium">0%</span>
</div> </div>
</div> </div>
<!-- tablo-desc-zone: inline-edit display + form for description (UI-SPEC §4) --> <!-- Description inline-edit zone -->
<div class="tablo-desc-zone mb-2"> <div class="tablo-desc-zone mb-4">
@TabloDescDisplay(tablo, csrfToken) @TabloDescDisplay(tablo, csrfToken)
</div> </div>
</div> </div>
<!-- Sticky tab navigation bar (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">
<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;"> <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;">
<!-- Overview tab -->
<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() }
@ -265,7 +312,6 @@ templ TabloDetailPage(user *auth.User, csrfToken string, tablo sqlc.Tablo, tasks
<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>
Overview Overview
</a> </a>
<!-- Tasks tab -->
<a <a
href={ templ.SafeURL("/tablos/" + tablo.ID.String() + "/tasks") } href={ templ.SafeURL("/tablos/" + tablo.ID.String() + "/tasks") }
hx-get={ "/tablos/" + tablo.ID.String() + "/tasks" } hx-get={ "/tablos/" + tablo.ID.String() + "/tasks" }
@ -278,10 +324,9 @@ templ TabloDetailPage(user *auth.User, csrfToken string, tablo sqlc.Tablo, tasks
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="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> <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>
Étapes Tasks
</a> </a>
<!-- Files tab -->
<a <a
href={ templ.SafeURL("/tablos/" + tablo.ID.String() + "/files") } href={ templ.SafeURL("/tablos/" + tablo.ID.String() + "/files") }
hx-get={ "/tablos/" + tablo.ID.String() + "/files" } hx-get={ "/tablos/" + tablo.ID.String() + "/files" }
@ -297,7 +342,6 @@ templ TabloDetailPage(user *auth.User, csrfToken string, tablo sqlc.Tablo, tasks
<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>
Files Files
</a> </a>
<!-- Discussion tab -->
<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" }
@ -313,7 +357,6 @@ templ TabloDetailPage(user *auth.User, csrfToken string, tablo sqlc.Tablo, tasks
<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>
Discussion Discussion
</a> </a>
<!-- Events tab -->
<a <a
href={ templ.SafeURL("/tablos/" + tablo.ID.String() + "/events") } href={ templ.SafeURL("/tablos/" + tablo.ID.String() + "/events") }
hx-get={ "/tablos/" + tablo.ID.String() + "/events" } hx-get={ "/tablos/" + tablo.ID.String() + "/events" }
@ -333,7 +376,7 @@ templ TabloDetailPage(user *auth.User, csrfToken string, tablo sqlc.Tablo, tasks
</div> </div>
</div> </div>
<!-- Tab content area — HTMX tab switches target this div --> <!-- Tab content area — HTMX tab switches target this div -->
<div id="tab-content" class="px-0 sm:px-0 pt-6 pb-8"> <div id="tab-content" class="px-4 sm:px-6 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" {
@ -377,7 +420,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-slate-600" class="tablo-title-zone text-xl md:text-3xl font-bold text-foreground cursor-pointer hover:text-[#804EEC] 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"
@ -411,7 +454,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-xl font-bold placeholder-slate-400 focus:border-[#804EEC] focus:outline-none" 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"
/> />
@FieldError(errs.Title) @FieldError(errs.Title)
</div> </div>
@ -581,10 +624,10 @@ templ TabloDeleteConfirmFragment(tablo sqlc.Tablo, csrfToken string) {
// TabloNotFoundPage renders a 404 page for tablos that don't exist or are not // TabloNotFoundPage renders a 404 page for tablos that don't exist or are not
// accessible by the current user (D-04: 404 not 403 to avoid existence leakage). // accessible by the current user (D-04: 404 not 403 to avoid existence leakage).
// user may be nil when called from an unauthenticated context — Layout handles nil. // activePath and sidebarTablos drive the AppLayout sidebar (pass "" and empty slice for not-found).
// UI-SPEC Copywriting Contract: "Not found" + "This tablo doesn't exist or you don't have access." // UI-SPEC Copywriting Contract: "Not found" + "This tablo doesn't exist or you don't have access."
templ TabloNotFoundPage(user *auth.User, csrfToken string) { templ TabloNotFoundPage(user *auth.User, csrfToken string, activePath string, sidebarTablos []sqlc.Tablo) {
@Layout("Not found", user, csrfToken) { @AppLayout("Not found", user, csrfToken, activePath, sidebarTablos) {
<div class="py-16 text-center"> <div class="py-16 text-center">
<h1 class="text-2xl font-semibold leading-snug text-slate-800">Not found</h1> <h1 class="text-2xl font-semibold leading-snug text-slate-800">Not found</h1>
<p class="mt-2 text-base text-slate-600">This tablo doesn&#39;t exist or you don&#39;t have access.</p> <p class="mt-2 text-base text-slate-600">This tablo doesn&#39;t exist or you don&#39;t have access.</p>