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
This commit is contained in:
parent
ae0ab0ca5b
commit
9c7b080f67
1 changed files with 105 additions and 51 deletions
|
|
@ -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) {
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h1 class="text-[28px] font-semibold leading-tight">Your Tablos</h1>
|
||||
@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",
|
||||
},
|
||||
})
|
||||
</div>
|
||||
<div id="create-form-slot"></div>
|
||||
<div id="tablos-list">
|
||||
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) {
|
||||
<section class="overview-section">
|
||||
<div class="overview-section-heading">
|
||||
<h3>Your Tablos</h3>
|
||||
@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",
|
||||
},
|
||||
})
|
||||
</div>
|
||||
<div id="create-form-slot"></div>
|
||||
<div id="tablos-list" class="project-grid">
|
||||
if len(cards) == 0 {
|
||||
@TablosEmptyState()
|
||||
} else {
|
||||
for _, card := range cards {
|
||||
@TabloProjectCard(card, csrfToken)
|
||||
}
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
}
|
||||
}
|
||||
|
||||
// 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() {
|
||||
<div class="text-center py-16">
|
||||
<h2 class="text-xl font-semibold leading-snug text-slate-800">No tablos yet</h2>
|
||||
<p class="mt-2 text-base text-slate-600">Create your first tablo to get started.</p>
|
||||
<div class="mt-6">
|
||||
@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) {
|
||||
<article id={ "tablo-" + card.Tablo.ID.String() } class="project-card">
|
||||
<div class="project-card-top">
|
||||
<div class="flex items-center gap-2">
|
||||
@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",
|
||||
},
|
||||
})
|
||||
<div class="tablo-delete-zone">
|
||||
@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",
|
||||
},
|
||||
})
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="project-card-title-row">
|
||||
if card.Tablo.Color.Valid && card.Tablo.Color.String != "" {
|
||||
<span class="project-avatar" style={ "background-color: " + card.Tablo.Color.String }></span>
|
||||
} else {
|
||||
<span class="project-avatar"></span>
|
||||
}
|
||||
<div class="tablo-title-zone">
|
||||
<h4>{ card.Tablo.Title }</h4>
|
||||
</div>
|
||||
</div>
|
||||
<div class="project-date-row">
|
||||
{ card.Tablo.CreatedAt.Time.Format("Jan 2, 2006") }
|
||||
</div>
|
||||
</article>
|
||||
}
|
||||
|
||||
// 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) {
|
||||
<div class="mb-4">
|
||||
<a href="/" class="text-sm text-slate-600 hover:underline">← Back to tablos</a>
|
||||
</div>
|
||||
|
|
@ -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) {
|
||||
<div class="py-16 text-center">
|
||||
<h1 class="text-2xl font-semibold leading-snug text-slate-800">Not found</h1>
|
||||
<p class="mt-2 text-base text-slate-600">This tablo doesn't exist or you don't have access.</p>
|
||||
|
|
|
|||
Loading…
Reference in a new issue