From 9c7b080f67b96006725059734b405e51c81988e2 Mon Sep 17 00:00:00 2001 From: Arthur Belleville Date: Sat, 16 May 2026 21:49:10 +0200 Subject: [PATCH] feat(15-03): restyle tablos.templ with AppLayout, TabloProjectCard, and EmptyState - Updated TablosDashboard signature to accept activePath and tablos for AppLayout - Replaced old @Layout call with @AppLayout (sidebar-based shell) - Added TabloProjectCard component with project-card grid, colored avatar, tablo-title-zone, edit/delete icon buttons - Replaced TablosEmptyState raw HTML with @ui.EmptyState component (ui-empty-state class) - Updated TabloDetailPage signature with activePath and sidebarTablos params - Updated TabloNotFoundPage signature with activePath and sidebarTablos params - Both detail pages switch from @Layout to @AppLayout --- backend/templates/tablos.templ | 156 ++++++++++++++++++++++----------- 1 file changed, 105 insertions(+), 51 deletions(-) diff --git a/backend/templates/tablos.templ b/backend/templates/tablos.templ index 1339b2d..3416121 100644 --- a/backend/templates/tablos.templ +++ b/backend/templates/tablos.templ @@ -6,61 +6,114 @@ import ( "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). +// TablosDashboard renders the root authenticated dashboard with sidebar AppLayout. +// Shows a project-card grid (or empty state) for the user's tablos. // UI-SPEC §1 Interaction Contract — GET /. -templ TablosDashboard(user *auth.User, csrfToken string, tablos []TabloCardView) { - @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) +templ TablosDashboard(user *auth.User, csrfToken string, activePath string, tablos []sqlc.Tablo, cards []TabloCardView) { + @AppLayout("Tablos — Xtablo", user, csrfToken, activePath, tablos) { +
+
+

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(cards) == 0 { + @TablosEmptyState() + } else { + for _, card := range cards { + @TabloProjectCard(card, csrfToken) + } } - } -
+
+ } } // 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() { -
-

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", - }, - }) + @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 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) { +
+
+
+ @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", + }, + }) +
+ @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", + }, + }) +
+
-
+
+ if card.Tablo.Color.Valid && card.Tablo.Color.String != "" { + + } else { + + } +
+

{ card.Tablo.Title }

+
+
+
+ { card.Tablo.CreatedAt.Time.Format("Jan 2, 2006") } +
+ } // TabloCard renders a single tablo as a ui.Card on the dashboard. @@ -182,10 +235,11 @@ templ TabloCardWithOOBFormClear(tablo sqlc.Tablo, csrfToken string) { // 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). +// 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, 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) { +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) { @@ -510,10 +564,10 @@ 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). -// 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." -templ TabloNotFoundPage(user *auth.User, csrfToken string) { - @Layout("Not found", user, csrfToken) { +templ TabloNotFoundPage(user *auth.User, csrfToken string, activePath string, sidebarTablos []sqlc.Tablo) { + @AppLayout("Not found", user, csrfToken, activePath, sidebarTablos) {

Not found

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