diff --git a/backend/templates/tablos.templ b/backend/templates/tablos.templ
index 491bf23..1a2fba5 100644
--- a/backend/templates/tablos.templ
+++ b/backend/templates/tablos.templ
@@ -65,7 +65,7 @@ templ TablosEmptyState() {
// TabloCard renders a single tablo as a ui.Card on the dashboard.
// Guards description and color rendering against null pgtype.Text values (Pitfall 6).
-// Wraps the Delete button in a .tablo-delete-zone div for Plan 03's delete-confirm swap.
+// 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()}) {
- @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",
- },
- })
-
+ @TabloDeleteButtonFragment(tablo, csrfToken)
View
@@ -177,3 +164,247 @@ templ TabloCardWithOOBFormClear(tablo sqlc.Tablo, csrfToken string) {
@TabloCard(tablo, csrfToken)
}
+
+// TabloDetailPage renders the full detail page for a single tablo.
+// Includes title zone, description zone, and delete zone.
+// UI-SPEC §3 Interaction Contract — GET /tablos/{id}.
+templ TabloDetailPage(user *auth.User, csrfToken string, tablo sqlc.Tablo) {
+ @Layout("Tablos — Xtablo", user, csrfToken) {
+
+
+ @TabloTitleDisplay(tablo, csrfToken)
+
+
+ @TabloDescDisplay(tablo, csrfToken)
+
+
+ @TabloDeleteButtonFragment(tablo, csrfToken)
+
+ }
+}
+
+// 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) {
+
+ @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.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.
+
+
+ }
+}
diff --git a/backend/templates/tablos_forms.go b/backend/templates/tablos_forms.go
index 80ab8b5..e3a4d3f 100644
--- a/backend/templates/tablos_forms.go
+++ b/backend/templates/tablos_forms.go
@@ -14,3 +14,12 @@ type TabloCreateErrors struct {
Title string
General string
}
+
+// TabloUpdateErrors holds per-field and general error messages for the tablo
+// inline-edit forms (title and description). A field with an empty string
+// means "no error for this field".
+type TabloUpdateErrors struct {
+ Title string
+ Description string
+ General string
+}