package templates import ( "backend/internal/auth" "backend/internal/db/sqlc" "backend/internal/web/ui" ) // TablosDashboard renders the root authenticated dashboard: heading, "New tablo" // button, create-form slot, and the list of tablo cards (or empty state). // UI-SPEC §1 Interaction Contract — GET /. templ TablosDashboard(user *auth.User, csrfToken string, tablos []sqlc.Tablo) { @Layout("Tablos — Xtablo", user, csrfToken) {

Your Tablos

@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", }, })
if len(tablos) == 0 { @TablosEmptyState() } else { for _, tablo := range tablos { @TabloCard(tablo, csrfToken) } }
} } // TablosEmptyState renders the empty-state copy when a user has no tablos. // Copy strings are locked by UI-SPEC Copywriting Contract. templ TablosEmptyState() {

No tablos yet

Create your first tablo to get started.

@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", "aria-label": "Create your first tablo", }, })
} // 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(tablo sqlc.Tablo, csrfToken string) { @ui.Card(templ.Attributes{"id": "tablo-" + tablo.ID.String()}) {

{ tablo.Title }

if tablo.Description.Valid && tablo.Description.String != "" {

{ tablo.Description.String }

} if tablo.Color.Valid && tablo.Color.String != "" {
{ tablo.Color.String }
}
@TabloDeleteButtonFragment(tablo, csrfToken)
View
} } // 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) {
@ui.CSRFField(csrfToken) @GeneralError(errs.General)

Create a tablo

@FieldError(errs.Title)
@FieldError(errs.Color)
@ui.Button(ui.ButtonProps{ Label: "Create tablo", Variant: ui.ButtonVariantDefault, Tone: ui.ButtonToneSolid, Size: ui.SizeMD, Type: "submit", }) Cancel
} // 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(tablo, csrfToken)
} // 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. // 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}. // 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, activeTab string) { @Layout("Tablos — Xtablo", user, csrfToken) {
← Back to tablos
@TabloTitleDisplay(tablo, csrfToken)
@TabloDescDisplay(tablo, csrfToken)
@TabloDeleteButtonFragment(tablo, csrfToken)
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 { @TabloOverviewTabFragment(tablo, csrfToken) }
} } // TabloOverviewTabFragment renders the overview tab content — description display. // Returned as a standalone fragment for HTMX tab-switch responses. templ TabloOverviewTabFragment(tablo sqlc.Tablo, csrfToken string) {
if tablo.Description.Valid && tablo.Description.String != "" {

{ tablo.Description.String }

} else {

No description.

}
} // 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) {
@EtapeStrip(tablo.ID, etapes, counts, filter, csrfToken, false) @KanbanBoard(tablo.ID, csrfToken, tasks, filter)
} // 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) {
@ui.CSRFField(csrfToken)
@FieldError(errs.Title)
@ui.Button(ui.ButtonProps{ Label: "Save changes", Variant: ui.ButtonVariantDefault, Tone: ui.ButtonToneSolid, Size: ui.SizeMD, Type: "submit", }) @ui.Button(ui.ButtonProps{ Label: "Discard changes", Variant: ui.ButtonVariantNeutral, Tone: ui.ButtonToneSoft, Size: ui.SizeMD, Type: "button", Attrs: templ.Attributes{ "hx-get": "/tablos/" + tablo.ID.String() + "/show-title", "hx-target": "closest .tablo-title-zone", "hx-swap": "outerHTML", }, })
} // 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) {
@ui.CSRFField(csrfToken)
@FieldError(errs.Description)
@ui.Button(ui.ButtonProps{ Label: "Save changes", Variant: ui.ButtonVariantDefault, Tone: ui.ButtonToneSolid, Size: ui.SizeMD, Type: "submit", }) @ui.Button(ui.ButtonProps{ Label: "Discard changes", Variant: ui.ButtonVariantNeutral, Tone: ui.ButtonToneSoft, Size: ui.SizeMD, Type: "button", Attrs: templ.Attributes{ "hx-get": "/tablos/" + tablo.ID.String() + "/show-desc", "hx-target": "closest .tablo-desc-zone", "hx-swap": "outerHTML", }, })
} // 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) {
@ui.Button(ui.ButtonProps{ Label: "Delete", Variant: ui.ButtonVariantDanger, Tone: ui.ButtonToneSoft, Size: ui.SizeMD, Type: "button", Attrs: templ.Attributes{ "hx-get": "/tablos/" + tablo.ID.String() + "/delete-confirm", "hx-target": "closest .tablo-delete-zone", "hx-swap": "outerHTML", "aria-label": "Delete tablo", }, })
} // 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) {

Delete tablo?

This cannot be undone.

@ui.CSRFField(csrfToken) @ui.Button(ui.ButtonProps{ Label: "Yes, delete", Variant: ui.ButtonVariantDanger, Tone: ui.ButtonToneSolid, Size: ui.SizeMD, Type: "submit", Attrs: templ.Attributes{ "aria-label": "Confirm delete tablo", }, })
@ui.Button(ui.ButtonProps{ Label: "Keep tablo", Variant: ui.ButtonVariantNeutral, Tone: ui.ButtonToneSoft, Size: ui.SizeMD, Type: "button", Attrs: templ.Attributes{ "hx-get": "/tablos/" + tablo.ID.String() + "/delete-cancel", "hx-target": "closest .tablo-delete-zone", "hx-swap": "outerHTML", "aria-label": "Keep tablo", }, })
} // 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). // user may be nil when called from an unauthenticated context — Layout handles nil. // UI-SPEC Copywriting Contract: "Not found" + "This tablo doesn't exist or you don't have access." templ TabloNotFoundPage(user *auth.User, csrfToken string) { @Layout("Not found", user, csrfToken) {

Not found

This tablo doesn't exist or you don't have access.

Back to tablos
} }