docs(15): create phase plan
This commit is contained in:
parent
431096e4de
commit
13b6f525de
3 changed files with 647 additions and 7 deletions
|
|
@ -3,14 +3,14 @@ gsd_state_version: 1.0
|
|||
milestone: v3.0
|
||||
milestone_name: Design System & Visual Polish
|
||||
status: executing
|
||||
last_updated: "2026-05-16T17:40:00.000Z"
|
||||
last_activity: 2026-05-16 -- Phase 14 Plan 02 execution complete, awaiting final verification
|
||||
last_updated: "2026-05-16T19:35:46.574Z"
|
||||
last_activity: 2026-05-16 -- Phase 15 planning complete
|
||||
progress:
|
||||
total_phases: 5
|
||||
completed_phases: 1
|
||||
total_plans: 7
|
||||
completed_phases: 2
|
||||
total_plans: 10
|
||||
completed_plans: 7
|
||||
percent: 100
|
||||
percent: 70
|
||||
---
|
||||
|
||||
# STATE
|
||||
|
|
@ -30,8 +30,8 @@ See: `.planning/PROJECT.md` (updated 2026-05-16)
|
|||
|
||||
Phase: 14
|
||||
Plan: 02 (complete)
|
||||
Status: Phase 14 execution complete — awaiting final visual verification
|
||||
Last activity: 2026-05-16 -- Phase 14 Plan 02 execution complete, awaiting final verification
|
||||
Status: Ready to execute
|
||||
Last activity: 2026-05-16 -- Phase 15 planning complete
|
||||
|
||||
## Previous Milestone Status
|
||||
|
||||
|
|
|
|||
563
.planning/phases/15-dashboard-tablos/15-PATTERNS.md
Normal file
563
.planning/phases/15-dashboard-tablos/15-PATTERNS.md
Normal file
|
|
@ -0,0 +1,563 @@
|
|||
# Phase 15: Dashboard & Tablos - Pattern Map
|
||||
|
||||
**Mapped:** 2026-05-16
|
||||
**Files analyzed:** 7 new/modified files
|
||||
**Analogs found:** 7 / 7
|
||||
|
||||
---
|
||||
|
||||
## File Classification
|
||||
|
||||
| New/Modified File | Role | Data Flow | Closest Analog | Match Quality |
|
||||
|---|---|---|---|---|
|
||||
| `backend/templates/app_layout.templ` | layout component | request-response | `backend/templates/auth_layout.templ` | role-match |
|
||||
| `backend/templates/app_layout_helpers.go` | utility | pure function | `go-backend/internal/web/views/home.go` | exact |
|
||||
| `backend/templates/tablos.templ` | component (modified) | request-response | `backend/templates/tablos.templ` (self, current) | exact |
|
||||
| `backend/internal/web/ui/app.css` | config/CSS | static | `backend/internal/web/ui/auth.css` | role-match |
|
||||
| `backend/tailwind.input.css` | config (modified) | static | `backend/tailwind.input.css` (self, current) | exact |
|
||||
| `backend/internal/web/handlers_tablos.go` | handler (modified) | request-response | `backend/internal/web/handlers_planning.go` | role-match |
|
||||
| `backend/internal/web/handlers_planning.go` | handler (modified) | request-response | `backend/internal/web/handlers_planning.go` (self) | exact |
|
||||
|
||||
---
|
||||
|
||||
## Pattern Assignments
|
||||
|
||||
### `backend/templates/app_layout.templ` (new — layout component, request-response)
|
||||
|
||||
**Analog:** `backend/templates/auth_layout.templ`
|
||||
|
||||
**Imports pattern** (`auth_layout.templ` lines 1 — no explicit imports; `layout.templ` lines 6–9):
|
||||
```go
|
||||
package templates
|
||||
|
||||
import (
|
||||
"backend/internal/auth"
|
||||
"backend/internal/db/sqlc"
|
||||
"backend/internal/web/ui"
|
||||
)
|
||||
```
|
||||
|
||||
**Core shell pattern** (`auth_layout.templ` lines 15–40 — the established top-level HTML shell convention):
|
||||
```go
|
||||
templ AuthLayout(title string, csrfToken string) {
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||
<title>{ title }</title>
|
||||
<link rel="stylesheet" href="/static/tailwind.css"/>
|
||||
</head>
|
||||
<body>
|
||||
<div class="login-screen">
|
||||
{ children... }
|
||||
</div>
|
||||
<script src="/static/htmx.min.js" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
}
|
||||
```
|
||||
|
||||
**AppLayout adaptation** — follow this pattern exactly but swap `.login-screen` for `.dashboard-shell`, add sidebar sub-component call, and load the three scripts from `layout.templ` lines 54–56:
|
||||
```go
|
||||
// Scripts to load (from layout.templ lines 54–56):
|
||||
<script src="/static/htmx.min.js" defer></script>
|
||||
<script src="/static/sortable.min.js" defer></script>
|
||||
<script src="/static/discussion-sse.js" defer></script>
|
||||
```
|
||||
|
||||
**Sidebar structure** (from `go-backend/internal/web/views/dashboard_components.templ` lines 54–91):
|
||||
```go
|
||||
templ DashboardSidebar(activePath string, tablos []tablomodel.Record) {
|
||||
<aside class="dashboard-sidebar">
|
||||
<nav aria-label="Main navigation" class="sidebar-nav-shell">
|
||||
<div class="sidebar-brand">
|
||||
<a class="sidebar-brand-link" aria-label="Home" href="/">
|
||||
<img alt="Logo XTablo" class="sidebar-brand-logo" src="/static/logo_dark.png"/>
|
||||
<h1 class="sidebar-brand-title">XTablo</h1>
|
||||
</a>
|
||||
<button class="sidebar-collapse-button" type="button">...</button>
|
||||
</div>
|
||||
<div class="sidebar-primary">
|
||||
<ul class="sidebar-list" role="list">
|
||||
// nav items loop
|
||||
</ul>
|
||||
@SidebarProjectsSection(tablos)
|
||||
<ul class="sidebar-list sidebar-footer-links" role="list">
|
||||
// footer nav items
|
||||
</ul>
|
||||
</div>
|
||||
@SidebarOrganization(user)
|
||||
</nav>
|
||||
</aside>
|
||||
}
|
||||
```
|
||||
|
||||
**Note:** go-backend's sidebar nav links use `hx-get` + `hx-target="#app-main-content"` for HTMX partial swaps. In the backend, per D-N02, routes that don't exist render as visual-only items (no href, cursor: default). For existing routes, use standard `<a href="...">` without HTMX (full-page navigation is fine for Phase 15 — no SPA swap is required).
|
||||
|
||||
**OOB constraint** (`tablos.templ` lines 172–179 — established pattern to follow):
|
||||
```go
|
||||
// OOB fragments MUST be top-level siblings, never nested inside AppLayout:
|
||||
templ TabloCardWithOOBFormClear(tablo sqlc.Tablo, csrfToken string) {
|
||||
@TabloCard(TabloCardFromTablo(tablo), csrfToken)
|
||||
<div id="create-form-slot" hx-swap-oob="true"></div>
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `backend/templates/app_layout_helpers.go` (new — utility, pure function)
|
||||
|
||||
**Analog:** `go-backend/internal/web/views/home.go` lines 17–39
|
||||
|
||||
**Exact pattern to copy** (go-backend `home.go` lines 1–39):
|
||||
```go
|
||||
package templates // NOTE: package is "templates" not "views"
|
||||
|
||||
import "strings"
|
||||
|
||||
func sidebarNavItemClass(active bool) string {
|
||||
if active {
|
||||
return "sidebar-nav-item is-active"
|
||||
}
|
||||
return "sidebar-nav-item"
|
||||
}
|
||||
|
||||
func isActivePath(activePath string, href string) bool {
|
||||
return strings.TrimSpace(activePath) != "" && activePath == href
|
||||
}
|
||||
|
||||
func sidebarNavItemID(href string) string {
|
||||
switch href {
|
||||
case "/":
|
||||
return "sidebar-nav-home"
|
||||
default:
|
||||
slug := strings.Trim(strings.ReplaceAll(href, "/", "-"), "-")
|
||||
if slug == "" {
|
||||
slug = "item"
|
||||
}
|
||||
return "sidebar-nav-" + slug
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**sidebarNavItem struct** (go-backend `home.go` lines 47–53):
|
||||
```go
|
||||
type sidebarNavItem struct {
|
||||
Href string
|
||||
Label string
|
||||
Icon string
|
||||
Active bool
|
||||
DividerAfter bool
|
||||
}
|
||||
```
|
||||
|
||||
**Nav items slice builder** (go-backend `home.go` lines 158–167 — adapt to English labels + backend routes):
|
||||
```go
|
||||
func sidebarPrimaryNavItems(activePath string) []sidebarNavItem {
|
||||
return []sidebarNavItem{
|
||||
{Href: "/", Label: "Dashboard", Icon: "panels", Active: isActivePath(activePath, "/"), DividerAfter: true},
|
||||
// Tasks, Chat, Files — visual-only (no Href, or Href: "" per D-N02)
|
||||
{Href: "/planning", Label: "Planning", Icon: "planning", Active: isActivePath(activePath, "/planning")},
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `backend/templates/tablos.templ` (modified — components, request-response)
|
||||
|
||||
**Analog:** `backend/templates/tablos.templ` (current file, lines 1–526) — same file, pattern evolution
|
||||
|
||||
**Signature change pattern** — follow the call-forwarding convention in `tablos.templ` line 13:
|
||||
```go
|
||||
// BEFORE (line 12–13):
|
||||
templ TablosDashboard(user *auth.User, csrfToken string, tablos []TabloCardView) {
|
||||
@Layout("Tablos — Xtablo", user, csrfToken) {
|
||||
|
||||
// AFTER — new signature forwards activePath + tablos slice to AppLayout:
|
||||
templ TablosDashboard(user *auth.User, csrfToken string, activePath string, tablos []sqlc.Tablo, cards []TabloCardView) {
|
||||
@AppLayout("Tablos — Xtablo", user, csrfToken, activePath, tablos) {
|
||||
```
|
||||
|
||||
**Section heading pattern** (from go-backend `dashboard_components.templ` lines 238–244 — `.overview-section-heading`):
|
||||
```go
|
||||
<section class="overview-section">
|
||||
<div class="overview-section-heading">
|
||||
<h3>Your Tablos</h3>
|
||||
// New tablo button + #create-form-slot go here (D-C05)
|
||||
</div>
|
||||
<div id="tablos-list" class="project-grid">
|
||||
// cards or empty state
|
||||
</div>
|
||||
</section>
|
||||
```
|
||||
|
||||
**Project card pattern** (adapted from go-backend `tablos.templ` lines 178–214, preserving backend HTMX attrs):
|
||||
```go
|
||||
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-3">
|
||||
@ui.IconButton(ui.IconButtonProps{
|
||||
Label: "Edit tablo",
|
||||
Icon: "pencil",
|
||||
Variant: ui.IconButtonVariantNeutral,
|
||||
Tone: ui.IconButtonToneGhost,
|
||||
Type: "button",
|
||||
Attrs: templ.Attributes{
|
||||
// PRESERVE existing HTMX attrs from current tablos.templ (Pitfall 3)
|
||||
},
|
||||
})
|
||||
@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 class="project-card-title-row">
|
||||
<span class="project-avatar"
|
||||
if card.Tablo.Color.Valid && card.Tablo.Color.String != "" {
|
||||
style={ "background-color: " + card.Tablo.Color.String }
|
||||
}>
|
||||
</span>
|
||||
<h4>{ card.Tablo.Title }</h4>
|
||||
</div>
|
||||
<div class="project-date-row">
|
||||
{ card.Tablo.CreatedAt.Time.Format("Jan 2, 2006") }
|
||||
</div>
|
||||
</article>
|
||||
}
|
||||
```
|
||||
|
||||
**Color null-safety pattern** — already established in current `tablos.templ` lines 85–90:
|
||||
```go
|
||||
// ESTABLISHED: always guard pgtype.Text with .Valid check
|
||||
if card.Tablo.Color.Valid && card.Tablo.Color.String != "" {
|
||||
style={ "background-color: " + card.Tablo.Color.String }
|
||||
}
|
||||
```
|
||||
|
||||
**CreatedAt formatting** — from Pitfall 6 in RESEARCH.md (pgtype.Timestamptz requires `.Time` accessor):
|
||||
```go
|
||||
// CORRECT:
|
||||
card.Tablo.CreatedAt.Time.Format("Jan 2, 2006")
|
||||
// WRONG (compiler error):
|
||||
card.Tablo.CreatedAt.Format("Jan 2, 2006")
|
||||
```
|
||||
|
||||
**EmptyState pattern** (`backend/internal/web/ui/empty_state.templ` lines 10–27):
|
||||
```go
|
||||
// Replace TablosEmptyState raw HTML with:
|
||||
@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",
|
||||
},
|
||||
}),
|
||||
})
|
||||
```
|
||||
|
||||
**SidebarProjectsSection sub-component** (adapted from go-backend `dashboard_components.templ` lines 103–115 — use `sqlc.Tablo` not `tablomodel.Record`):
|
||||
```go
|
||||
templ SidebarProjectsSection(tablos []sqlc.Tablo) {
|
||||
<div id="sidebar-projects-section" class="sidebar-projects">
|
||||
<hr role="separator"/>
|
||||
<div class="sidebar-section-label">Projects</div>
|
||||
<ul class="sidebar-project-list">
|
||||
for _, tablo := range tablos {
|
||||
<li>
|
||||
<a class="sidebar-project-link" href={ templ.SafeURL("/tablos/" + tablo.ID.String()) }>
|
||||
<span class="sidebar-project-icon"
|
||||
if tablo.Color.Valid && tablo.Color.String != "" {
|
||||
style={ "background-color: " + tablo.Color.String }
|
||||
}>
|
||||
</span>
|
||||
<span class="sidebar-project-label">{ tablo.Title }</span>
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
```
|
||||
|
||||
**SidebarOrganization footer** (adapted from go-backend `dashboard_components.templ` lines 131–143 — use `user.Email` and include logout):
|
||||
```go
|
||||
templ SidebarOrganizationFooter(user *auth.User, csrfToken string) {
|
||||
<div class="sidebar-organization">
|
||||
<div class="organization-button">
|
||||
<span class="organization-avatar">{ string([]rune(user.Email)[:1]) }</span>
|
||||
<span class="organization-copy">
|
||||
<span class="organization-name">{ user.Email }</span>
|
||||
</span>
|
||||
<form method="POST" action="/logout" class="inline">
|
||||
@ui.CSRFField(csrfToken)
|
||||
<button type="submit" class="text-sm">Log out</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
```
|
||||
|
||||
**TabloDetailPage / TabloNotFoundPage layout switch** (current `tablos.templ` lines 187–188, 515–516):
|
||||
```go
|
||||
// BEFORE:
|
||||
@Layout("Tablos — Xtablo", user, csrfToken) {
|
||||
// AFTER (D-L01 — all authenticated pages switch):
|
||||
@AppLayout("Tablos — Xtablo", user, csrfToken, activePath, tablos) {
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `backend/internal/web/ui/app.css` (new — CSS)
|
||||
|
||||
**Analog:** `backend/internal/web/ui/auth.css` — same pattern: verbatim port of CSS sections from go-backend into a new file
|
||||
|
||||
**Go-backend source:** `go-backend/internal/web/ui/app.css`
|
||||
|
||||
**Sections to port verbatim** (line ranges from go-backend app.css):
|
||||
|
||||
| Section | Lines | Classes |
|
||||
|---|---|---|
|
||||
| Dashboard shell grid | 455–465 | `.dashboard-shell`, `.dashboard-sidebar` |
|
||||
| Sidebar nav shell | 467–479 | `.sidebar-nav-shell` |
|
||||
| Sidebar brand | 481–509 | `.sidebar-brand`, `.sidebar-brand-link`, `.sidebar-brand-logo`, `.sidebar-brand-title` |
|
||||
| Sidebar collapse button | 511–527 | `.sidebar-collapse-button` (non-functional in Phase 15) |
|
||||
| Sidebar primary + list | 533–558 | `.sidebar-primary`, `.sidebar-list`, `.sidebar-divider` |
|
||||
| Sidebar nav items | 560–605 | `.sidebar-nav-item`, `.sidebar-nav-item.is-active`, `.sidebar-nav-link`, `.sidebar-nav-link-inner`, `.sidebar-nav-icon`, `.sidebar-nav-label` |
|
||||
| Sidebar projects | 607–668 | `.sidebar-projects`, `.sidebar-section-label`, `.sidebar-project-list`, `.sidebar-project-link`, `.sidebar-project-icon`, `.sidebar-project-label` |
|
||||
| Sidebar footer links | 670–673 | `.sidebar-footer-links` |
|
||||
| Sidebar organization | 675–732 | `.sidebar-organization`, `.organization-button`, `.organization-avatar`, `.organization-name`, `.organization-meta` |
|
||||
| Dashboard main | 734–741 | `.dashboard-main` |
|
||||
| Overview section heading | 875–892 | `.overview-section`, `.overview-section-heading` |
|
||||
| Project grid | 894–899 | `.project-grid` |
|
||||
| Project card | 900–914 | `.project-card`, `.project-card-top` |
|
||||
| Project card icon button overrides | 945–963 | **Adapt** go-backend's `.borderless-icon-button` overrides to use backend class names (see Shared Patterns below) |
|
||||
| Project title/avatar | 986–1005 | `.project-card-title-row`, `.project-avatar` |
|
||||
| Project date row | 1039–1046 | `.project-date-row` |
|
||||
|
||||
**Critical:** All color values in go-backend app.css already use `var(--...)` tokens — port verbatim without substitution.
|
||||
|
||||
---
|
||||
|
||||
### `backend/tailwind.input.css` (modified — config)
|
||||
|
||||
**Analog:** `backend/tailwind.input.css` (self, current — lines 1–20)
|
||||
|
||||
**Pattern to follow** (current file lines 7–20):
|
||||
```css
|
||||
@import "./internal/web/ui/base.css";
|
||||
@import "./internal/web/ui/auth.css";
|
||||
/* ... existing imports ... */
|
||||
@import "./internal/web/ui/spacing.css";
|
||||
```
|
||||
|
||||
**Change:** Add one new line after the existing imports:
|
||||
```css
|
||||
@import "./internal/web/ui/app.css";
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `backend/internal/web/handlers_tablos.go` (modified — handler)
|
||||
|
||||
**Analog:** `backend/internal/web/handlers_tablos.go` lines 39–55 (current `TablosListHandler`)
|
||||
|
||||
**Current pattern** (lines 39–55):
|
||||
```go
|
||||
func TablosListHandler(deps TablosDeps) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
_, user, _ := auth.Authed(r.Context())
|
||||
|
||||
tabloRows, err := deps.Queries.ListTablosByUserWithDiscussionUnread(r.Context(), user.ID)
|
||||
if err != nil {
|
||||
slog.Default().Error("tablos list: query failed", "user_id", user.ID, "err", err)
|
||||
http.Error(w, "internal server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if tabloRows == nil {
|
||||
tabloRows = []sqlc.ListTablosByUserWithDiscussionUnreadRow{}
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
_ = templates.TablosDashboard(user, csrf.Token(r), templates.TabloCardsFromUnreadRows(tabloRows)).Render(r.Context(), w)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Change pattern** — derive `[]sqlc.Tablo` from already-fetched cardViews (no second query per RESEARCH.md Pattern 5):
|
||||
```go
|
||||
cardViews := templates.TabloCardsFromUnreadRows(tabloRows)
|
||||
|
||||
// Derive sidebar tablos from already-fetched data (no extra DB query)
|
||||
tablos := make([]sqlc.Tablo, 0, len(cardViews))
|
||||
for _, cv := range cardViews {
|
||||
tablos = append(tablos, cv.Tablo)
|
||||
}
|
||||
|
||||
_ = templates.TablosDashboard(user, csrf.Token(r), "/", tablos, cardViews).Render(r.Context(), w)
|
||||
```
|
||||
|
||||
**Error handling pattern** — unchanged, copy from lines 44–48 verbatim.
|
||||
|
||||
---
|
||||
|
||||
### `backend/internal/web/handlers_planning.go` (modified — handler)
|
||||
|
||||
**Analog:** `backend/internal/web/handlers_planning.go` lines 34–58 (self, current)
|
||||
|
||||
**Current pattern** (lines 34–58):
|
||||
```go
|
||||
func PlanningPageHandler(deps PlanningDeps) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
_, user, _ := auth.Authed(r.Context())
|
||||
// ... fetch events ...
|
||||
_ = templates.PlanningPage(user, csrf.Token(r), agenda).Render(r.Context(), w)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Change pattern** — add tablos fetch before template render:
|
||||
```go
|
||||
// Fetch tablos for sidebar (PlanningDeps already has Queries *sqlc.Queries)
|
||||
tablos, err := deps.Queries.ListTablosByUser(r.Context(), user.ID)
|
||||
if err != nil {
|
||||
slog.Default().Error("planning: ListTablosByUser failed", "user_id", user.ID, "err", err)
|
||||
http.Error(w, "internal server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if tablos == nil {
|
||||
tablos = []sqlc.Tablo{}
|
||||
}
|
||||
|
||||
// Pass tablos + activePath to template
|
||||
_ = templates.PlanningPage(user, csrf.Token(r), "/planning", tablos, agenda).Render(r.Context(), w)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Shared Patterns
|
||||
|
||||
### CSS @import registration
|
||||
**Source:** `backend/tailwind.input.css` lines 7–20
|
||||
**Apply to:** New `app.css` file
|
||||
```css
|
||||
/* Add to tailwind.input.css after existing imports: */
|
||||
@import "./internal/web/ui/app.css";
|
||||
```
|
||||
|
||||
### pgtype.Text null-safety
|
||||
**Source:** `backend/templates/tablos.templ` lines 85–90
|
||||
**Apply to:** All color-rendering spans in `app_layout.templ` and `tablos.templ`
|
||||
```go
|
||||
if card.Tablo.Color.Valid && card.Tablo.Color.String != "" {
|
||||
style={ "background-color: " + card.Tablo.Color.String }
|
||||
}
|
||||
```
|
||||
|
||||
### pgtype.Timestamptz formatting
|
||||
**Source:** RESEARCH.md Pitfall 6 (verified against `sqlc.Tablo` type)
|
||||
**Apply to:** Project card date row in `tablos.templ`
|
||||
```go
|
||||
// Always use .Time to unwrap pgtype.Timestamptz:
|
||||
card.Tablo.CreatedAt.Time.Format("Jan 2, 2006")
|
||||
```
|
||||
|
||||
### CSRF field
|
||||
**Source:** `backend/templates/tablos.templ` line 121 / `layout.templ` lines 41–42
|
||||
**Apply to:** All form fragments inside `app_layout.templ` (logout form)
|
||||
```go
|
||||
@ui.CSRFField(csrfToken)
|
||||
```
|
||||
|
||||
### IconButton class names (VERIFIED)
|
||||
**Source:** `backend/internal/web/ui/variants.go` lines 179–187
|
||||
**Apply to:** `app.css` project-card icon button overrides
|
||||
|
||||
```
|
||||
IconButtonClass(IconButtonVariantNeutral, IconButtonToneGhost)
|
||||
→ "borderless-icon-button ui-icon-button-ghost ui-icon-button-neutral"
|
||||
|
||||
IconButtonClass(IconButtonVariantDanger, IconButtonToneGhost)
|
||||
→ "borderless-icon-button ui-icon-button-ghost ui-icon-button-danger"
|
||||
```
|
||||
|
||||
The `.borderless-icon-button` class is already in `icon-button.css`. So `app.css` project-card overrides should target:
|
||||
```css
|
||||
/* Adapted from go-backend app.css lines 945–963 — using verified backend class names */
|
||||
.project-card-top .borderless-icon-button {
|
||||
padding: 0;
|
||||
}
|
||||
.project-card-top .ui-icon-button-ghost.ui-icon-button-neutral:hover {
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
.project-card-top .ui-icon-button-ghost.ui-icon-button-danger:hover {
|
||||
color: var(--color-status-danger-icon-hover);
|
||||
}
|
||||
```
|
||||
|
||||
### Handler error + render pattern
|
||||
**Source:** `backend/internal/web/handlers_tablos.go` lines 44–54
|
||||
**Apply to:** Any new tablos-fetch added to other handlers
|
||||
```go
|
||||
tablos, err := deps.Queries.ListTablosByUser(r.Context(), user.ID)
|
||||
if err != nil {
|
||||
slog.Default().Error("<handler>: ListTablosByUser failed", "user_id", user.ID, "err", err)
|
||||
http.Error(w, "internal server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if tablos == nil {
|
||||
tablos = []sqlc.Tablo{}
|
||||
}
|
||||
```
|
||||
|
||||
### templ.SafeURL for dynamic links
|
||||
**Source:** `backend/templates/tablos.templ` line 98
|
||||
**Apply to:** All dynamic `href` attributes in `app_layout.templ`
|
||||
```go
|
||||
href={ templ.SafeURL("/tablos/" + tablo.ID.String()) }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## No Analog Found
|
||||
|
||||
All Phase 15 files have analogs in the existing codebase. No file requires building from RESEARCH.md patterns alone.
|
||||
|
||||
---
|
||||
|
||||
## Key Notes for Planner
|
||||
|
||||
1. **`account_providers.templ`** also calls `@Layout(...)` (verified via grep in RESEARCH.md) — planner should include it in the layout-switch task if Phase 15 scope covers all authenticated pages.
|
||||
|
||||
2. **SVG icons:** The `SidebarNavItem` templ calls `@SidebarIcon(item.Icon)` in go-backend. The backend's equivalent is `@ui.UIIcon(kind)` (already in `icon_button.templ` lines 18–74). The nav items can call `@ui.UIIcon(item.Icon)` directly, or a new `SidebarIcon` wrapper can delegate to it. The icon names from go-backend (`"panels"`, `"tasks"`, `"layers"`, `"planning"`, `"chat"`, `"files"`) must either match a `case` in `UIIcon` or be added.
|
||||
|
||||
3. **Visual-only nav items** (D-N02): For Tasks, Files, Settings — render as `<div class="sidebar-nav-item">` without an `<a>` tag, or with `href=""` and `style="cursor: default"`. No HTMX attributes.
|
||||
|
||||
4. **`templ generate`** must be run after every `.templ` file change (Pitfall 5 from RESEARCH.md).
|
||||
|
||||
---
|
||||
|
||||
## Metadata
|
||||
|
||||
**Analog search scope:** `backend/templates/`, `backend/internal/web/`, `go-backend/internal/web/views/`, `go-backend/internal/web/ui/`
|
||||
**Files scanned:** 12
|
||||
**Pattern extraction date:** 2026-05-16
|
||||
77
.planning/phases/15-dashboard-tablos/15-VALIDATION.md
Normal file
77
.planning/phases/15-dashboard-tablos/15-VALIDATION.md
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
---
|
||||
phase: 15
|
||||
slug: dashboard-tablos
|
||||
status: draft
|
||||
nyquist_compliant: false
|
||||
wave_0_complete: false
|
||||
created: 2026-05-16
|
||||
---
|
||||
|
||||
# Phase 15 — Validation Strategy
|
||||
|
||||
> Per-phase validation contract for feedback sampling during execution.
|
||||
|
||||
---
|
||||
|
||||
## Test Infrastructure
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| **Framework** | Go `testing` + `net/http/httptest` (existing) |
|
||||
| **Config file** | none — standard Go test runner |
|
||||
| **Quick run command** | `cd backend && go test ./internal/web/... -run TestTablos -v` |
|
||||
| **Full suite command** | `cd backend && go test ./...` |
|
||||
| **Estimated runtime** | ~5 seconds |
|
||||
|
||||
---
|
||||
|
||||
## Sampling Rate
|
||||
|
||||
- **After every task commit:** Run `cd backend && go test ./internal/web/... -run TestTablos -count=1`
|
||||
- **After every plan wave:** Run `cd backend && go test ./... -count=1`
|
||||
- **Before `/gsd-verify-work`:** Full suite must be green
|
||||
- **Max feedback latency:** 10 seconds
|
||||
|
||||
---
|
||||
|
||||
## Per-Task Verification Map
|
||||
|
||||
| Task ID | Plan | Wave | Requirement | Threat Ref | Secure Behavior | Test Type | Automated Command | File Exists | Status |
|
||||
|---------|------|------|-------------|------------|-----------------|-----------|-------------------|-------------|--------|
|
||||
| 15-W0-01 | W0 | 0 | DASH-01 | — | N/A | integration | `go test ./internal/web/... -run TestTablosDashboard_Sidebar` | ❌ Wave 0 | ⬜ pending |
|
||||
| 15-W0-02 | W0 | 0 | DASH-02 | — | N/A | integration | `go test ./internal/web/... -run TestTablosDashboard_ProjectCards` | ❌ Wave 0 | ⬜ pending |
|
||||
| 15-W0-03 | W0 | 0 | DASH-03 | — | N/A | integration | `go test ./internal/web/... -run TestTablosDashboard_EmptyState` | ❌ Wave 0 | ⬜ pending |
|
||||
| 15-regression | — | 1 | (regression) | — | N/A | integration | `go test ./internal/web/... -run TestTablos` | ✅ | ⬜ pending |
|
||||
|
||||
*Status: ⬜ pending · ✅ green · ❌ red · ⚠️ flaky*
|
||||
|
||||
---
|
||||
|
||||
## Wave 0 Requirements
|
||||
|
||||
- [ ] `backend/internal/web/handlers_tablos_test.go` — add `TestTablosDashboard_Sidebar` (asserts sidebar brand, nav items, tablo list, footer present in GET / response body)
|
||||
- [ ] `backend/internal/web/handlers_tablos_test.go` — add `TestTablosDashboard_ProjectCards` (asserts `.project-card` elements rendered for each tablo)
|
||||
- [ ] `backend/internal/web/handlers_tablos_test.go` — add `TestTablosDashboard_EmptyState` (asserts `.ui-empty-state` present when no tablos exist)
|
||||
|
||||
---
|
||||
|
||||
## Manual-Only Verifications
|
||||
|
||||
| Behavior | Requirement | Why Manual | Test Instructions |
|
||||
|----------|-------------|------------|-------------------|
|
||||
| Sidebar visual layout matches go-backend screenshot | DASH-01 | CSS layout/visual fidelity requires browser inspection | Open `/` in browser; compare sidebar structure, spacing, and icon rendering against go-backend reference |
|
||||
| Project card color avatars render correctly | DASH-02 | Color rendering requires browser | Check tablo cards show colored circles; null color shows neutral gray |
|
||||
| Empty state matches design system | DASH-03 | Visual fidelity | Check empty state icon, title, description, and CTA button match Phase 13 `ui.EmptyState` design |
|
||||
|
||||
---
|
||||
|
||||
## Validation Sign-Off
|
||||
|
||||
- [ ] All tasks have `<automated>` verify or Wave 0 dependencies
|
||||
- [ ] Sampling continuity: no 3 consecutive tasks without automated verify
|
||||
- [ ] Wave 0 covers all MISSING references
|
||||
- [ ] No watch-mode flags
|
||||
- [ ] Feedback latency < 10s
|
||||
- [ ] `nyquist_compliant: true` set in frontmatter
|
||||
|
||||
**Approval:** pending
|
||||
Loading…
Reference in a new issue