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) {
-
- 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.