if len(cards) == 0 {
@TablosEmptyState()
} else {
for _, card := range cards {
@TabloProjectCard(card, csrfToken)
}
}
Projet
Statut
Créé le
Progression
for _, card := range cards {
@TabloListRow(card, csrfToken)
}
Mes Tâches
Les tâches apparaîtront ici.
Contacts
Vos contacts apparaîtront ici.
}
}
// TablosEmptyState renders the empty-state copy when a user has no tablos.
// Copy strings are locked by UI-SPEC Copywriting Contract.
// Uses ui.EmptyState for consistent styling across the app (Phase 13).
templ TablosEmptyState() {
@ui.EmptyState(ui.EmptyStateProps{
Title: "No tablos yet",
Description: "Create your first tablo to get started.",
Action: ui.Button(ui.ButtonProps{
Label: "New tablo",
Variant: ui.ButtonVariantDefault,
Tone: ui.ButtonToneSolid,
Size: ui.SizeMD,
Type: "button",
Attrs: templ.Attributes{
"hx-get": "/tablos/new",
"hx-target": "#create-form-slot",
"hx-swap": "innerHTML",
},
}),
})
}
// TabloProjectCard renders a single tablo as a card in the grid view.
// Guards color rendering against null pgtype.Text values (Pitfall 6).
// Uses .Time accessor on pgtype.Timestamptz (Pitfall 6).
templ TabloProjectCard(card TabloCardView, csrfToken string) {
}
// TabloCard renders a single tablo as a ui.Card on the dashboard.
// Guards description and color rendering against null pgtype.Text values (Pitfall 6).
// Delegates delete-zone rendering to TabloDeleteButtonFragment (single source of truth).
templ TabloCard(card TabloCardView, csrfToken string) {
}
templ DiscussionUnreadBadge(count int64) {
if count > 0 {
{ DiscussionUnreadDisplay(count) }
}
}
// TabloCreateFormFragment renders the inline create form loaded into #create-form-slot
// via HTMX. Falls back to a plain POST /tablos for non-JS paths.
// UI-SPEC §2 Interaction Contract — GET /tablos/new + POST /tablos.
templ TabloCreateFormFragment(form TabloCreateForm, errs TabloCreateErrors, csrfToken string) {
}
// TabloCardWithOOBFormClear renders a TabloCard as the primary swap target
// AND an OOB element that clears #create-form-slot in the same response.
// The OOB div MUST be a top-level sibling of TabloCard — NOT nested (Pitfall 5).
// HTMX applies the primary swap (HX-Retarget: #tablos-list, afterbegin) AND
// the OOB swap (#create-form-slot → empty) from a single response.
templ TabloCardWithOOBFormClear(tablo sqlc.Tablo, csrfToken string) {
@TabloCard(TabloCardFromTablo(tablo), csrfToken)
}
// 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) {
if tablo.Color.Valid && tablo.Color.String != "" {
if len(tablo.Title) > 0 {
{ string([]rune(tablo.Title)[0:1]) }
}
} else {
if len(tablo.Title) > 0 {
{ string([]rune(tablo.Title)[0:1]) }
}
}
}
// TabloOverviewTabFragment renders the overview tab content — description inline-edit zone.
// 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) {
@TabloDescDisplay(tablo, csrfToken)
}
// TasksTabFragment wraps the KanbanBoard for use as a standalone HTMX tab fragment.
// Returned by TabloTasksTabHandler on HX-Request == "true".
// 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) {
}
// TabloTitleDisplay renders the tablo title as a clickable element that swaps
// to the edit form on click. The outermost element carries class tablo-title-zone
// so hx-target="closest .tablo-title-zone" + hx-swap="outerHTML" round-trips cleanly.
// UI-SPEC §4 Interaction Contract — title inline-edit display state.
templ TabloTitleDisplay(tablo sqlc.Tablo, csrfToken string) {
{ tablo.Title }
}
// TabloTitleEditFragment renders the inline title edit form. Carries class
// tablo-title-zone on the form element so outerHTML round-trips work correctly.
// Includes hidden description field (current value) + hidden _zone="title" for
// TabloUpdateHandler to know which display fragment to render on success.
// UI-SPEC §4 Interaction Contract — title inline-edit edit state.
templ TabloTitleEditFragment(tablo sqlc.Tablo, errs TabloUpdateErrors, csrfToken string) {
}
// TabloDescDisplay renders the tablo description as a clickable element that
// swaps to the edit form on click. Carries class tablo-desc-zone on the outermost
// element for clean outerHTML round-trips.
// UI-SPEC §4 Interaction Contract — description inline-edit display state.
templ TabloDescDisplay(tablo sqlc.Tablo, csrfToken string) {
if tablo.Description.Valid && tablo.Description.String != "" {
{ tablo.Description.String }
} else {
Add a description
}
}
// TabloDescEditFragment renders the inline description edit form. Carries class
// tablo-desc-zone on the form element so outerHTML round-trips work correctly.
// Includes hidden title field (current value) + hidden _zone="desc" for
// TabloUpdateHandler to know which display fragment to render on success.
// UI-SPEC §4 Interaction Contract — description inline-edit edit state.
templ TabloDescEditFragment(tablo sqlc.Tablo, errs TabloUpdateErrors, csrfToken string) {
}
// TabloDeleteButtonFragment renders the delete trigger button wrapped in a
// .tablo-delete-zone div. This is the canonical single source of truth for the
// delete zone shape — TabloCard delegates to this component (no duplication).
// UI-SPEC §5 Interaction Contract — delete display state.
templ TabloDeleteButtonFragment(tablo sqlc.Tablo, csrfToken string) {
}
// TabloDeleteConfirmFragment renders the inline confirmation dialog that appears
// when the user clicks Delete. Carries class tablo-delete-zone for outerHTML
// round-trips. "Yes, delete" submits the DELETE action; "Keep tablo" restores
// the original Delete button via TabloDeleteCancelHandler.
// UI-SPEC §5 Interaction Contract — delete confirmation state.
templ TabloDeleteConfirmFragment(tablo sqlc.Tablo, csrfToken string) {
}
// 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).
// 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."
templ TabloNotFoundPage(user *auth.User, csrfToken string, activePath string, sidebarTablos []sqlc.Tablo) {
@AppLayout("Not found", user, csrfToken, activePath, sidebarTablos, "Not found", nil, nil) {
Not found
This tablo doesn't exist or you don't have access.