docs(phase-18): complete phase execution — sidebar + header restyle approved

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Arthur Belleville 2026-05-17 16:04:09 +02:00
parent 9ba650b345
commit 3542f3b105
No known key found for this signature in database
6 changed files with 1365 additions and 13 deletions

View file

@ -8,7 +8,7 @@
## v4.0 Active Phases
- [ ] Phase 18: App Shell & Navigation — sidebar redesign + top header bar (NAV-01, NAV-02)
- [x] Phase 18: App Shell & Navigation — sidebar redesign + top header bar (NAV-01, NAV-02) — completed 2026-05-17
- [ ] Phase 19: Tablo List Revamp — card redesign, progress bar, status field, list/card toggle (LIST-01, LIST-02, LIST-03)
- [ ] Phase 20: Tablo Detail & Kanban — detail page + kanban restyled to Figma (DETAIL-01, TASK-01)
- [ ] Phase 21: Task Grid & Roadmap Views — new grid view + new roadmap view with date fields (TASK-02, TASK-03)
@ -56,11 +56,17 @@
### Phase 18: App Shell & Navigation
**Goal:** Redesign the sidebar and top header bar to match the Figma design.
**Requirements:** NAV-01, NAV-02
**Plans:** 3 plans
**Success criteria:**
1. Sidebar renders brand section, icon nav items, tablo list section, and user footer matching Figma
2. Every authenticated page shows a top header bar with page title and contextual actions
3. Existing navigation functionality (logout, tablo selection) is preserved
Plans:
- [ ] 18-01-PLAN.md — AppLayout signature extension + BreadcrumbItem struct + all call sites updated
- [ ] 18-02-PLAN.md — Sidebar full HTML/CSS rebuild to Figma spec with collapse toggle
- [ ] 18-03-PLAN.md — PageHeader component, avatar dropdown, /settings stub, tests updated
### Phase 19: Tablo List Revamp
**Goal:** Restyle the tablos page with revamped cards, real progress data, list/card toggle, and status field.
**Requirements:** LIST-01, LIST-02, LIST-03
@ -118,7 +124,7 @@
| 15. Dashboard & Tablos | v3.0 | 3/3 | Complete | 2026-05-16 |
| 16. Tablo Detail | v3.0 | 4/4 | Complete | 2026-05-17 |
| 17. Chat & Planning | v3.0 | 2/2 | Complete | 2026-05-17 |
| 18. App Shell & Navigation | v4.0 | | Pending | — |
| 18. App Shell & Navigation | v4.0 | 0/3 | Pending | — |
| 19. Tablo List Revamp | v4.0 | — | Pending | — |
| 20. Tablo Detail & Kanban | v4.0 | — | Pending | — |
| 21. Task Grid & Roadmap Views | v4.0 | — | Pending | — |

View file

@ -2,15 +2,14 @@
gsd_state_version: 1.0
milestone: v4.0
milestone_name: Figma Design Parity
status: planning
last_updated: "2026-05-17T12:22:07.603Z"
last_activity: 2026-05-17
status: Executing
last_updated: "2026-05-17T16:05:00.000Z"
last_activity: 2026-05-17 — Phase 18 complete (sidebar + header restyle approved)
progress:
total_phases: 0
completed_phases: 0
total_plans: 0
completed_plans: 0
percent: 0
total_phases: 5
completed_phases: 1
total_plans: 3
completed_plans: 3
---
# STATE
@ -28,10 +27,10 @@ See: `.planning/PROJECT.md` (updated 2026-05-17)
## Current Position
Phase: Not started (defining requirements)
Phase: 19 — Tablo List Revamp (next)
Plan: —
Status: Defining requirements
Last activity: 2026-05-17 — Milestone v4.0 started
Status: Executing
Last activity: 2026-05-17 — Phase 18 complete
## Previous Milestone Status

View file

@ -0,0 +1,318 @@
---
phase: 18-app-shell-navigation
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
- backend/templates/app_layout_helpers.go
- backend/templates/app_layout.templ
- backend/templates/tablos.templ
- backend/templates/planning.templ
- backend/templates/account_providers.templ
- backend/internal/web/handlers_tablos.go
- backend/internal/web/handlers_planning.go
- backend/internal/web/handlers_account.go
- backend/internal/web/handlers_discussion.go
- backend/internal/web/handlers_files.go
- backend/internal/web/handlers_events.go
autonomous: true
requirements:
- NAV-01
- NAV-02
must_haves:
truths:
- "The Go codebase compiles with zero errors after AppLayout signature is extended"
- "Every authenticated page still renders (no 500 errors) — dashboard, tablo detail, planning, account providers"
- "go test ./... passes with no failures"
artifacts:
- path: "backend/templates/app_layout_helpers.go"
provides: "BreadcrumbItem struct export"
contains: "type BreadcrumbItem struct"
- path: "backend/templates/app_layout.templ"
provides: "Extended AppLayout signature with pageTitle, breadcrumb, headerActions"
contains: "pageTitle string"
- path: "backend/templates/tablos.templ"
provides: "TablosDashboard and TabloDetailPage updated with breadcrumb params"
key_links:
- from: "backend/internal/web/handlers_tablos.go"
to: "backend/templates/tablos.templ"
via: "TablosDashboard / TabloDetailPage call with breadcrumb args"
pattern: "BreadcrumbItem"
- from: "backend/templates/tablos.templ"
to: "backend/templates/app_layout.templ"
via: "AppLayout call with pageTitle and breadcrumb"
pattern: "AppLayout.*pageTitle"
---
<objective>
Extend AppLayout with three new parameters (pageTitle, breadcrumb, headerActions) and update every call site mechanically so the codebase compiles. No visual changes yet — the new params are threaded through but rendered minimally (pageTitle in the HTML title tag, breadcrumb as plain inline text).
Purpose: Establishes the Go/templ contracts that Plans 02 and 03 build on. Plans 02 and 03 cannot safely start until this compiles clean.
Output: A green compile + passing test suite. All handlers supply correct per-page breadcrumbs.
</objective>
<execution_context>
@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.claude/get-shit-done/workflows/execute-plan.md
@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/ROADMAP.md
@.planning/REQUIREMENTS.md
@.planning/phases/18-app-shell-navigation/18-CONTEXT.md
@.planning/phases/18-app-shell-navigation/18-RESEARCH.md
</context>
<interfaces>
<!-- Current AppLayout signature (to be replaced) -->
<!-- From backend/templates/app_layout.templ line 153 -->
templ AppLayout(title string, user *auth.User, csrfToken string, activePath string, tablos []sqlc.Tablo)
<!-- Current templ call sites that wrap AppLayout -->
<!-- tablos.templ line 12 -->
templ TablosDashboard(user *auth.User, csrfToken string, activePath string, tablos []sqlc.Tablo, cards []TabloCardView)
<!-- tablos.templ line 241 -->
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)
<!-- planning.templ line 8 -->
templ PlanningPage(user *auth.User, csrfToken string, activePath string, tablos []sqlc.Tablo, agenda PlanningAgenda)
<!-- account_providers.templ line 8 -->
templ AccountProvidersPage(user *auth.User, providers []LinkedProviderStatus, csrfToken string, activePath string, tablos []sqlc.Tablo)
<!-- Handler call sites (handlers_tablos.go) -->
templates.TablosDashboard(user, csrf.Token(r), "/", sidebarTablos, cardViews)
templates.TabloDetailPage(user, csrf.Token(r), "", sidebarTablos, tablo, tasks, nil, templates.EtapeTaskCounts{}, templates.EtapeFilter{}, nil, templates.EventsCalendar{}, templates.DiscussionTabData{}, "overview")
<!-- Handler call sites (other handlers) -->
templates.PlanningPage(user, csrf.Token(r), "/planning", sidebarTablos, agenda)
templates.AccountProvidersPage(user, statuses, csrf.Token(r), "/", sidebarTablos)
templates.TabloDetailPage(user, csrf.Token(r), "", discussionSidebarTablos, tablo, nil, nil, templates.EtapeTaskCounts{}, templates.EtapeFilter{}, nil, templates.EventsCalendar{}, data, "discussion")
</interfaces>
<tasks>
<task type="auto">
<name>Task 1: Define BreadcrumbItem and extend AppLayout signature</name>
<files>backend/templates/app_layout_helpers.go, backend/templates/app_layout.templ</files>
<action>
In app_layout_helpers.go, add the BreadcrumbItem struct immediately after the sidebarNavItem type declaration (around line 6):
// BreadcrumbItem represents one crumb in the top header breadcrumb trail.
// If Href is empty, the item renders as plain text (current page — no link).
type BreadcrumbItem struct {
Label string
Href string
}
In app_layout.templ, update the AppLayout signature from:
templ AppLayout(title string, user *auth.User, csrfToken string, activePath string, tablos []sqlc.Tablo)
to:
templ AppLayout(title string, user *auth.User, csrfToken string, activePath string, tablos []sqlc.Tablo, pageTitle string, breadcrumb []BreadcrumbItem, headerActions templ.Component)
The templ import for "github.com/a-h/templ" is already pulled in transitively via the ui package usage. If the compiler complains, add it explicitly to the import block at the top of app_layout.templ.
Inside the AppLayout body, make a minimal placeholder so breadcrumb params are consumed (prevents unused-variable compile errors). Immediately before the DashboardSidebar call or inside the dashboard-main div, add a comment-only breadcrumb stub:
if len(breadcrumb) > 0 {
<!-- breadcrumb: Plan 03 will render this properly -->
}
if headerActions != nil {
@headerActions
}
The nil-guard on headerActions is required (per research Pitfall 6 — rendering nil templ.Component panics).
Do NOT change any visual output yet. The goal is compiler compatibility only.
</action>
<verify>
<automated>cd /Users/arthur.belleville/Documents/perso/projects/xtablo-source/backend && templ generate ./... 2>&1 | head -20</automated>
</verify>
<done>app_layout_helpers.go exports BreadcrumbItem struct. app_layout.templ AppLayout signature has 8 parameters. templ generate produces no errors.</done>
</task>
<task type="auto">
<name>Task 2: Update all templ wrapper signatures and their internal AppLayout calls</name>
<files>backend/templates/tablos.templ, backend/templates/planning.templ, backend/templates/account_providers.templ</files>
<action>
Each templ function that calls AppLayout must (a) accept pageTitle and breadcrumb in its own signature, and (b) pass them through to AppLayout. Update each file as follows.
--- tablos.templ ---
TablosDashboard (line 12): Add two params at the end of its signature:
templ TablosDashboard(user *auth.User, csrfToken string, activePath string, tablos []sqlc.Tablo, cards []TabloCardView, pageTitle string, breadcrumb []BreadcrumbItem)
Update its AppLayout call (line 13) to:
@AppLayout("Tablos — Xtablo", user, csrfToken, activePath, tablos, pageTitle, breadcrumb, nil)
TabloDetailPage (line 241): Add two params at the end:
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, pageTitle string, breadcrumb []BreadcrumbItem)
Update its two AppLayout calls (lines ~242 and ~648) to pass pageTitle, breadcrumb, nil as the last three args.
The TabloNotFoundPage function near line 648 also wraps AppLayout — update it too. It can pass hardcoded values:
@AppLayout("Not found", user, csrfToken, activePath, sidebarTablos, "Not found", nil, nil)
--- planning.templ ---
PlanningPage (line 8): Add two params at end:
templ PlanningPage(user *auth.User, csrfToken string, activePath string, tablos []sqlc.Tablo, agenda PlanningAgenda, pageTitle string, breadcrumb []BreadcrumbItem)
Update AppLayout call (line 9) to pass pageTitle, breadcrumb, nil.
--- account_providers.templ ---
AccountProvidersPage (line 8): Add two params at end:
templ AccountProvidersPage(user *auth.User, providers []LinkedProviderStatus, csrfToken string, activePath string, tablos []sqlc.Tablo, pageTitle string, breadcrumb []BreadcrumbItem)
Update AppLayout call (line 9) to pass pageTitle, breadcrumb, nil.
Keep all other content in each file exactly as-is.
</action>
<verify>
<automated>cd /Users/arthur.belleville/Documents/perso/projects/xtablo-source/backend && templ generate ./... 2>&1 | tail -5</automated>
</verify>
<done>All three templ files compile without errors. TablosDashboard, TabloDetailPage, PlanningPage, and AccountProvidersPage all accept and pass through pageTitle and breadcrumb.</done>
</task>
<task type="auto">
<name>Task 3: Update all handler call sites with per-page breadcrumbs</name>
<files>backend/internal/web/handlers_tablos.go, backend/internal/web/handlers_planning.go, backend/internal/web/handlers_account.go, backend/internal/web/handlers_discussion.go, backend/internal/web/handlers_files.go, backend/internal/web/handlers_events.go</files>
<action>
Each handler that calls a templ function must now pass two additional arguments. Use the breadcrumb values below. The BreadcrumbItem type is referenced as templates.BreadcrumbItem in handler files.
--- handlers_tablos.go ---
TablosListHandler (line ~60, calls TablosDashboard):
templates.TablosDashboard(user, csrf.Token(r), "/", sidebarTablos, cardViews,
"Dashboard",
[]templates.BreadcrumbItem{{Label: "Dashboard", Href: ""}},
)
Error fallback call at line ~465 (same handler pattern):
templates.TablosDashboard(user, csrf.Token(r), "/", errorSidebarTablos, errorCardViews,
"Dashboard",
[]templates.BreadcrumbItem{{Label: "Dashboard", Href: ""}},
)
TabloDetailHandler (line ~225, calls TabloDetailPage):
templates.TabloDetailPage(user, csrf.Token(r), "", sidebarTablos, tablo, tasks, nil,
templates.EtapeTaskCounts{}, templates.EtapeFilter{}, nil, templates.EventsCalendar{},
templates.DiscussionTabData{}, "overview",
tablo.Title,
[]templates.BreadcrumbItem{{Label: "Dashboard", Href: "/"}, {Label: tablo.Title, Href: ""}},
)
TabloUpdateHandler (line ~338, calls TabloDetailPage):
templates.TabloDetailPage(user, csrf.Token(r), "", updateSidebarTablos, tablo, tasks, nil,
templates.EtapeTaskCounts{}, templates.EtapeFilter{}, nil, templates.EventsCalendar{},
templates.DiscussionTabData{}, "overview",
tablo.Title,
[]templates.BreadcrumbItem{{Label: "Dashboard", Href: "/"}, {Label: tablo.Title, Href: ""}},
)
--- handlers_planning.go ---
PlanningPageHandler (line ~68, calls PlanningPage):
templates.PlanningPage(user, csrf.Token(r), "/planning", sidebarTablos, agenda,
"Planning",
[]templates.BreadcrumbItem{{Label: "Planning", Href: ""}},
)
--- handlers_account.go ---
AccountProvidersHandler (line ~49, calls AccountProvidersPage):
templates.AccountProvidersPage(user, statuses, csrf.Token(r), "/", sidebarTablos,
"Linked Providers",
[]templates.BreadcrumbItem{{Label: "Linked Providers", Href: ""}},
)
--- handlers_discussion.go ---
Discussion handler (line ~78, calls TabloDetailPage):
templates.TabloDetailPage(user, csrf.Token(r), "", discussionSidebarTablos, tablo, nil, nil,
templates.EtapeTaskCounts{}, templates.EtapeFilter{}, nil, templates.EventsCalendar{},
data, "discussion",
tablo.Title,
[]templates.BreadcrumbItem{{Label: "Dashboard", Href: "/"}, {Label: tablo.Title, Href: ""}},
)
--- handlers_files.go ---
Files tab handler (line ~106, calls TabloDetailPage):
templates.TabloDetailPage(user, csrf.Token(r), "", filesSidebarTablos, tablo, nil, nil,
templates.EtapeTaskCounts{}, templates.EtapeFilter{}, fileList, templates.EventsCalendar{},
templates.DiscussionTabData{}, "files",
tablo.Title,
[]templates.BreadcrumbItem{{Label: "Dashboard", Href: "/"}, {Label: tablo.Title, Href: ""}},
)
Tasks-tab-from-files handler (line ~138, calls TabloDetailPage):
templates.TabloDetailPage(user, csrf.Token(r), "", tasksSidebarTablos, tablo, tasks, etapes,
counts, filter, nil, templates.EventsCalendar{}, templates.DiscussionTabData{}, "tasks",
tablo.Title,
[]templates.BreadcrumbItem{{Label: "Dashboard", Href: "/"}, {Label: tablo.Title, Href: ""}},
)
--- handlers_events.go ---
Events tab handler (line ~177, calls TabloDetailPage):
templates.TabloDetailPage(user, csrf.Token(r), "", eventsSidebarTablos, tablo, nil, nil,
templates.EtapeTaskCounts{}, templates.EtapeFilter{}, nil, calendar,
templates.DiscussionTabData{}, "events",
tablo.Title,
[]templates.BreadcrumbItem{{Label: "Dashboard", Href: "/"}, {Label: tablo.Title, Href: ""}},
)
After all edits, run the full build to confirm zero compile errors:
cd backend && go build ./...
The build MUST succeed before considering this task complete. If any call site is missed, the compiler reports "too many arguments" or "not enough arguments" — fix each reported site.
</action>
<verify>
<automated>cd /Users/arthur.belleville/Documents/perso/projects/xtablo-source/backend && go build ./... && go test ./... 2>&1 | tail -20</automated>
</verify>
<done>go build ./... succeeds with no errors. go test ./... passes. Every authenticated page (dashboard, tablo detail, planning, account providers) renders without 500 errors when the server is started.</done>
</task>
</tasks>
<threat_model>
## Trust Boundaries
| Boundary | Description |
|----------|-------------|
| Handler → Template | Handlers supply breadcrumb strings — these originate from DB data (tablo.Title) which is user-controlled content rendered via templ's auto-escaping |
## STRIDE Threat Register
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-18-01-01 | Tampering | tablo.Title in breadcrumb | accept | templ auto-escapes all string interpolations; no raw HTML insertion |
| T-18-01-02 | Denial of Service | templ generate fails mid-compile leaving partial _templ.go files | mitigate | Always run templ generate before go build; check templ output first |
</threat_model>
<verification>
1. `cd backend && templ generate ./...` — zero errors
2. `cd backend && go build ./...` — zero compile errors
3. `cd backend && go test ./...` — all tests pass
4. Manual smoke: `cd backend && go run . serve` then visit `http://localhost:8080` — dashboard renders without 500
5. Verify grep: `grep -r "BreadcrumbItem" backend/templates/app_layout_helpers.go` — shows struct definition
6. Verify grep: `grep -c "pageTitle" backend/templates/app_layout.templ` — returns 1 or more (param present)
</verification>
<success_criteria>
- BreadcrumbItem struct is exported from backend/templates package
- AppLayout has 8 parameters (was 5)
- TablosDashboard, TabloDetailPage, PlanningPage, AccountProvidersPage all accept and forward pageTitle + breadcrumb
- All 10 handler call sites pass the two new arguments
- go build ./... succeeds
- go test ./... passes
- No authenticated page returns a 500
</success_criteria>
<output>
After completion, create `.planning/phases/18-app-shell-navigation/18-01-SUMMARY.md`
</output>

View file

@ -0,0 +1,326 @@
---
phase: 18-app-shell-navigation
plan: 02
type: execute
wave: 2
depends_on:
- 18-01
files_modified:
- backend/templates/app_layout.templ
- backend/templates/app_layout_helpers.go
- backend/internal/web/ui/app.css
autonomous: true
requirements:
- NAV-01
must_haves:
truths:
- "Sidebar shows a GENERAL section label above Home, My Tasks, Projects, Events, Team Members nav items"
- "Sidebar shows a PROJECTS section label above the tablo list"
- "Chat and Files nav items are removed from the sidebar"
- "Clicking the collapse button toggles the sidebar between full-width and icon-only states"
- "Sidebar collapse does not trigger a server round-trip — state resets on page reload"
- "go build ./... succeeds after sidebar rebuild"
artifacts:
- path: "backend/templates/app_layout.templ"
provides: "Rebuilt DashboardSidebar with GENERAL + PROJECTS section labels and collapse button"
contains: "GENERAL"
- path: "backend/templates/app_layout_helpers.go"
provides: "Updated sidebarPrimaryNavItems with Figma-spec items"
contains: "My Tasks"
- path: "backend/internal/web/ui/app.css"
provides: "is-collapsed CSS rules, sidebar section label styles"
contains: "is-collapsed"
key_links:
- from: "backend/templates/app_layout.templ"
to: "backend/internal/web/ui/app.css"
via: "is-collapsed class toggled by inline JS, picked up by CSS rule"
pattern: "is-collapsed"
- from: "backend/templates/app_layout_helpers.go"
to: "backend/templates/app_layout.templ"
via: "sidebarPrimaryNavItems called in DashboardSidebar"
pattern: "sidebarPrimaryNavItems"
---
<objective>
Full HTML/CSS rebuild of the sidebar to match the Figma design (Homepage.png, Board.png). Replaces the Phase 15 DashboardSidebar internals with the new two-section structure (GENERAL + PROJECTS), wires the collapse toggle via inline JS, and updates app.css with collapsed-state rules.
Purpose: Delivers NAV-01. After this plan the sidebar visually matches Figma.
Output: Rebuilt DashboardSidebar templ component + updated sidebarPrimaryNavItems + new CSS rules in app.css.
</objective>
<execution_context>
@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.claude/get-shit-done/workflows/execute-plan.md
@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/ROADMAP.md
@.planning/phases/18-app-shell-navigation/18-CONTEXT.md
@.planning/phases/18-app-shell-navigation/18-RESEARCH.md
@.planning/phases/18-app-shell-navigation/18-01-SUMMARY.md
</context>
<interfaces>
<!-- Read screenshots/Homepage.png and screenshots/Board.png before implementing visual details -->
<!-- They are at /Users/arthur.belleville/Documents/perso/projects/xtablo-source/screenshots/ -->
<!-- Current DashboardSidebar signature (unchanged in Plan 01) -->
templ DashboardSidebar(activePath string, tablos []sqlc.Tablo, user *auth.User, csrfToken string)
<!-- Existing CSS classes to preserve (used in existing tests) -->
.dashboard-sidebar — outer aside element
.sidebar-nav-shell — inner nav element
.sidebar-brand — brand/logo section
.sidebar-primary — nav list container
.sidebar-nav-item — individual nav item
.sidebar-nav-item.is-active
.sidebar-projects — projects section container
.sidebar-section-label — section label text (GENERAL, PROJECTS)
<!-- Existing CSS classes to update/add in app.css -->
.dashboard-shell — grid container (currently: minmax(16rem, 18rem) 1fr)
.dashboard-shell.sidebar-is-collapsed — new rule: grid-template-columns: 4rem 1fr
.sidebar-collapse-button — exists but unwired; now gets onclick JS
.is-collapsed on .dashboard-sidebar — triggers label/text hiding
<!-- sidebarNavItem struct (unchanged) -->
type sidebarNavItem struct {
Href string
Label string
Icon string
Active bool
DividerAfter bool
}
<!-- Existing icon kinds in SidebarNavIcon: panels, tasks, layers, planning, chat, files -->
<!-- "layers" is the icon for Projects per Figma (stacked layers = projects/tablos) -->
<!-- "tasks" exists and maps to My Tasks -->
<!-- No "team" icon exists — add a new "team" case to SidebarNavIcon using a users SVG -->
</interfaces>
<tasks>
<task type="auto">
<name>Task 1: Update sidebarPrimaryNavItems and add team icon to SidebarNavIcon</name>
<files>backend/templates/app_layout_helpers.go, backend/templates/app_layout.templ</files>
<action>
In app_layout_helpers.go, replace the sidebarPrimaryNavItems function body per D-08. The new nav items are:
func sidebarPrimaryNavItems(activePath string) []sidebarNavItem {
return []sidebarNavItem{
{Href: "/", Label: "Home", Icon: "panels", Active: isActivePath(activePath, "/")},
{Href: "#", Label: "My Tasks", Icon: "tasks", Active: false},
{Href: "/", Label: "Projects", Icon: "layers", Active: false},
{Href: "/planning", Label: "Events", Icon: "planning", Active: isActivePath(activePath, "/planning")},
{Href: "#", Label: "Team Members", Icon: "team", Active: false},
}
}
Notes:
- "Projects" links to "/" (the tablos dashboard — no /tablos GET route exists per router.go).
- "My Tasks" and "Team Members" use href="#" (no route yet in v4.0 per D-08).
- DividerAfter is no longer needed (section labels replace dividers); remove it from all items.
- Chat and Files are removed entirely.
In app_layout.templ, add a "team" case to the SidebarNavIcon switch, after the "files" case:
case "team":
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"></path>
<circle cx="9" cy="7" r="4"></circle>
<path d="M22 21v-2a4 4 0 0 0-3-3.87"></path>
<path d="M16 3.13a4 4 0 0 1 0 7.75"></path>
</svg>
This is a standard Lucide "users" SVG — consistent with the existing icon set's stroke style.
</action>
<verify>
<automated>cd /Users/arthur.belleville/Documents/perso/projects/xtablo-source/backend && templ generate ./... && go build ./... 2>&1 | tail -5</automated>
</verify>
<done>sidebarPrimaryNavItems returns 5 items (Home, My Tasks, Projects, Events, Team Members). SidebarNavIcon handles "team" without falling to default. Build passes.</done>
</task>
<task type="auto">
<name>Task 2: Rebuild DashboardSidebar HTML with GENERAL/PROJECTS sections and collapse button</name>
<files>backend/templates/app_layout.templ</files>
<action>
Replace the DashboardSidebar templ component body with the new Figma-spec structure. The signature stays the same:
templ DashboardSidebar(activePath string, tablos []sqlc.Tablo, user *auth.User, csrfToken string)
New body structure:
<aside class="dashboard-sidebar">
<nav aria-label="Main navigation" class="sidebar-nav-shell">
<!-- Brand / logo -->
<div class="sidebar-brand">
<a class="sidebar-brand-link" href="/" aria-label="Home">
<img class="sidebar-brand-logo" src="/static/logo_dark.png" alt="Logo XTablo"/>
<h1 class="sidebar-brand-title">XTablo</h1>
</a>
<button
class="sidebar-collapse-button"
aria-label="Toggle sidebar"
onclick="(function(btn){var shell=document.querySelector('.dashboard-shell');if(shell){shell.classList.toggle('sidebar-is-collapsed');}})(this)"
type="button"
>
<!-- chevron-left icon (collapses) — use existing icon or inline SVG -->
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" width="16" height="16">
<path d="m15 18-6-6 6-6"/>
</svg>
</button>
</div>
<!-- GENERAL section -->
<div class="sidebar-primary">
<div class="sidebar-section-label">General</div>
<ul class="sidebar-list" role="list">
for _, item := range sidebarPrimaryNavItems(activePath) {
<li>
@SidebarNavItemRow(item)
</li>
}
</ul>
<!-- PROJECTS section (tablo list) -->
<div class="sidebar-projects">
<div class="sidebar-section-label">Projects</div>
<ul class="sidebar-project-list">
for _, tablo := range tablos {
<li>
<a href={ templ.SafeURL("/tablos/" + tablo.ID.String()) } class="sidebar-project-link">
if tablo.Color.Valid && tablo.Color.String != "" {
<span class="sidebar-project-icon" style={ "background-color: " + tablo.Color.String }></span>
} else {
<span class="sidebar-project-icon"></span>
}
<span class="sidebar-project-label">{ tablo.Title }</span>
</a>
</li>
}
</ul>
</div>
</div>
</nav>
</aside>
Key changes vs Phase 15:
- SidebarProjectsSection and SidebarOrganizationFooter components are no longer called from DashboardSidebar. SidebarOrganizationFooter (logout form) moves to the avatar dropdown in Plan 03. For now, remove it — Plan 03 adds it to the header dropdown.
- The PROJECTS section is inlined directly inside DashboardSidebar (the separate SidebarProjectsSection component still exists in the file for now; it simply won't be called).
- The HR separator before the projects section is removed; section labels replace dividers.
- Collapse button uses inline onclick JS that toggles 'sidebar-is-collapsed' on the .dashboard-shell element. No Alpine.js. No server round-trip. State resets on reload per D-09.
- The DividerAfter rendering branch in DashboardSidebar is removed (the sidebarNavItem struct field can stay in the struct for now; it just won't be used).
Do NOT remove the SidebarOrganizationFooter, SidebarProjectsSection, or SidebarNavItemRow templ functions from the file — Plan 03 will clean up or repurpose them.
</action>
<verify>
<automated>cd /Users/arthur.belleville/Documents/perso/projects/xtablo-source/backend && templ generate ./... && go build ./... 2>&1 | tail -5</automated>
</verify>
<done>DashboardSidebar no longer calls SidebarOrganizationFooter. New structure has sidebar-section-label divs for both General and Projects sections. Collapse button is wired. Build passes.</done>
</task>
<task type="auto">
<name>Task 3: Add sidebar collapsed-state CSS to app.css</name>
<files>backend/internal/web/ui/app.css</files>
<action>
Locate the end of the existing sidebar CSS block in backend/internal/web/ui/app.css (currently ends around the .dashboard-main rule). Add the following new rules in a clearly labelled section after the existing sidebar block. Do NOT edit backend/static/tailwind.css directly — it is a compiled artifact.
Add these rules:
/* ── Sidebar collapse state ────────────────────────────────────────── */
/* When sidebar-is-collapsed is toggled on the shell, shrink the grid column */
.dashboard-shell.sidebar-is-collapsed {
grid-template-columns: 4rem 1fr;
}
/* Hide text labels and section headings in collapsed state */
.dashboard-shell.sidebar-is-collapsed .sidebar-brand-title,
.dashboard-shell.sidebar-is-collapsed .sidebar-section-label,
.dashboard-shell.sidebar-is-collapsed .sidebar-nav-label,
.dashboard-shell.sidebar-is-collapsed .sidebar-project-label {
display: none;
}
/* Center icons in collapsed state */
.dashboard-shell.sidebar-is-collapsed .sidebar-nav-link-inner {
justify-content: center;
}
/* Hide the full project list in collapsed state (icons not present for projects) */
.dashboard-shell.sidebar-is-collapsed .sidebar-project-list {
display: none;
}
/* Flip the collapse button chevron when collapsed */
.dashboard-shell.sidebar-is-collapsed .sidebar-collapse-button svg {
transform: rotate(180deg);
}
/* Ensure sidebar-section-label uses correct token styling if not already set */
/* (Only add if not already defined above in the file) */
.sidebar-section-label {
font-size: 0.6875rem; /* 11px */
font-weight: 600;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--color-text-muted);
padding: 0.75rem 0.75rem 0.25rem;
}
After adding these rules, regenerate tailwind.css by running the CSS build command. The build pipeline is:
cd backend && npx tailwindcss -i tailwind.input.css -o static/tailwind.css --minify
If the just/justfile command is available: `just build:css` or equivalent. Check the justfile for the exact command. The goal is to get the new CSS into the compiled tailwind.css that the browser loads.
Verify the compiled file now contains the new classes:
grep -c "sidebar-is-collapsed" backend/static/tailwind.css
This must return a number greater than 0.
</action>
<verify>
<automated>grep -c "sidebar-is-collapsed" /Users/arthur.belleville/Documents/perso/projects/xtablo-source/backend/static/tailwind.css</automated>
</verify>
<done>app.css contains the sidebar-is-collapsed block. tailwind.css is regenerated and contains "sidebar-is-collapsed" class rules. go build ./... still passes.</done>
</task>
</tasks>
<threat_model>
## Trust Boundaries
| Boundary | Description |
|----------|-------------|
| Inline JS onclick → DOM | Collapse toggle runs inline script; no user input is processed |
| Tablo color field → CSS style attribute | tablo.Color.String is interpolated into a style attribute for the project icon color |
## STRIDE Threat Register
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-18-02-01 | Tampering | tablo.Color in style attribute | accept | Color is stored in DB by the authenticated user themselves; templ does not sanitize style values but the content is user's own data |
| T-18-02-02 | Tampering | tailwind.css overwritten directly | mitigate | Always edit app.css; run build pipeline to regenerate tailwind.css — never hand-edit the artifact |
| T-18-02-03 | DoS | Sidebar collapse breaks layout (grid column not shrinking) | mitigate | CSS rule uses .dashboard-shell.sidebar-is-collapsed targeting the shell wrapper, not just the sidebar child — per Research Pitfall 3 |
</threat_model>
<verification>
1. `cd backend && templ generate ./... && go build ./...` — zero errors
2. `grep -c "sidebar-is-collapsed" backend/static/tailwind.css` — returns > 0
3. `grep "My Tasks\|Team Members\|GENERAL\|General" backend/templates/app_layout_helpers.go` — shows updated nav items
4. `cd backend && go test ./internal/web/ -run TestTablosDashboard_Sidebar -v` — existing test passes (class names .dashboard-sidebar and .sidebar-nav-shell are preserved)
5. Visual check: start server, visit dashboard — sidebar shows General section label, 5 nav items, Projects section label with tablo list; clicking collapse button shrinks sidebar to icon-only width
</verification>
<success_criteria>
- sidebarPrimaryNavItems returns exactly 5 items: Home, My Tasks, Projects, Events, Team Members
- Chat and Files items are gone from the nav
- DashboardSidebar renders GENERAL section label above nav items and PROJECTS section label above tablo list
- SidebarOrganizationFooter is no longer called from DashboardSidebar
- Collapse button is present and toggles sidebar-is-collapsed on .dashboard-shell via inline JS
- Collapsed state hides labels, shrinks grid column to 4rem
- go build ./... and go test ./... both pass
</success_criteria>
<output>
After completion, create `.planning/phases/18-app-shell-navigation/18-02-SUMMARY.md`
</output>

View file

@ -0,0 +1,555 @@
---
phase: 18-app-shell-navigation
plan: 03
type: execute
wave: 3
depends_on:
- 18-01
- 18-02
files_modified:
- backend/templates/app_layout.templ
- backend/internal/web/ui/app.css
- backend/internal/web/handlers_tablos_test.go
- backend/internal/web/router.go
autonomous: true
requirements:
- NAV-01
- NAV-02
must_haves:
truths:
- "Every authenticated page shows a top header bar with breadcrumb, search placeholder, and right-side icons"
- "Breadcrumb renders the correct path for each page (e.g., 'Dashboard' on the home page, 'Dashboard > Project Details' on tablo detail)"
- "Clicking the avatar circle opens a dropdown with workspace info, settings link, and logout button"
- "Logout from the avatar dropdown still works — POST /logout succeeds"
- "Settings link in dropdown navigates to /settings which renders a stub page"
- "go test ./... passes including updated sidebar test and new header test"
artifacts:
- path: "backend/templates/app_layout.templ"
provides: "PageHeader templ component with three zones and avatar dropdown"
contains: "PageHeader"
- path: "backend/internal/web/ui/app.css"
provides: "CSS for .page-header, .breadcrumb, .header-avatar-menu, .header-avatar-dropdown"
contains: "page-header"
- path: "backend/internal/web/handlers_tablos_test.go"
provides: "Updated TestTablosDashboard_Sidebar + new TestTablosDashboard_Header test"
contains: "page-header"
- path: "backend/internal/web/router.go"
provides: "/settings stub route"
contains: "/settings"
key_links:
- from: "backend/templates/app_layout.templ"
to: "backend/internal/web/ui/app.css"
via: ".page-header, .header-avatar-menu, .breadcrumb classes rendered in PageHeader"
pattern: "page-header"
- from: "backend/templates/app_layout.templ"
to: "/logout route"
via: "form method=POST action=/logout inside avatar dropdown, CSRF token via ui.CSRFField"
pattern: "action.*logout"
- from: "PageHeader"
to: "AppLayout"
via: "called inside dashboard-main div, above children"
pattern: "@PageHeader"
---
<objective>
Implement the PageHeader templ component (three-zone top bar: breadcrumb left, search placeholder center, bell/inbox/avatar right), wire it into AppLayout, add all supporting CSS to app.css, implement the avatar dropdown using native details/summary HTML, add a /settings stub route, and update tests to assert the new shell structure.
Purpose: Delivers NAV-02. After this plan every authenticated page has the top header bar matching Figma, the avatar dropdown works, and logout continues to function.
Output: PageHeader component, avatar dropdown, /settings stub, updated tests, new CSS in app.css.
</objective>
<execution_context>
@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.claude/get-shit-done/workflows/execute-plan.md
@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/ROADMAP.md
@.planning/phases/18-app-shell-navigation/18-CONTEXT.md
@.planning/phases/18-app-shell-navigation/18-RESEARCH.md
@.planning/phases/18-app-shell-navigation/18-01-SUMMARY.md
@.planning/phases/18-app-shell-navigation/18-02-SUMMARY.md
</context>
<interfaces>
<!-- Read screenshots/Homepage.png and screenshots/Board.png before implementing -->
<!-- Located at /Users/arthur.belleville/Documents/perso/projects/xtablo-source/screenshots/ -->
<!-- AppLayout signature after Plan 01 -->
templ AppLayout(
title string,
user *auth.User,
csrfToken string,
activePath string,
tablos []sqlc.Tablo,
pageTitle string,
breadcrumb []BreadcrumbItem,
headerActions templ.Component,
)
<!-- BreadcrumbItem struct (defined in Plan 01) -->
type BreadcrumbItem struct {
Label string
Href string
}
<!-- CSRFField usage (existing pattern) -->
@ui.CSRFField(csrfToken) // renders a hidden input with CSRF token
<!-- auth.User struct -->
type User struct {
ID uuid.UUID
Email string
PasswordHash pgtype.Text
CreatedAt time.Time
UpdatedAt time.Time
}
// Only Email is available. No Name or AvatarURL.
<!-- Existing CSS design tokens available -->
var(--color-surface-elevated)
var(--color-border-panel)
var(--color-text-muted)
var(--color-text-brand)
var(--shadow-floating-control)
var(--color-surface-elevated-strong)
var(--color-border-panel-muted)
<!-- Existing test file location -->
backend/internal/web/handlers_tablos_test.go
// Existing test: TestTablosDashboard_Sidebar (line ~602) asserts "dashboard-sidebar" and "sidebar-nav-shell"
</interfaces>
<tasks>
<task type="auto">
<name>Task 1: Implement PageHeader component and wire into AppLayout</name>
<files>backend/templates/app_layout.templ</files>
<action>
Add the PageHeader templ component to app_layout.templ. It renders the full-width top bar with three zones. Place the function after DashboardSidebar and before AppLayout in the file.
PageHeader signature:
templ PageHeader(pageTitle string, breadcrumb []BreadcrumbItem, headerActions templ.Component, user *auth.User, csrfToken string)
Structure:
<header class="page-header">
<!-- LEFT: breadcrumb zone -->
<div class="page-header-left">
<nav class="breadcrumb" aria-label="Breadcrumb">
for i, crumb := range breadcrumb {
if i > 0 {
<span class="breadcrumb-separator" aria-hidden="true">/</span>
}
if crumb.Href != "" {
<a href={ templ.SafeURL(crumb.Href) } class="breadcrumb-item breadcrumb-item--link">{ crumb.Label }</a>
} else {
<span class="breadcrumb-item breadcrumb-item--current" aria-current="page">{ crumb.Label }</span>
}
}
if len(breadcrumb) == 0 {
<span class="breadcrumb-item breadcrumb-item--current">{ pageTitle }</span>
}
</nav>
</div>
<!-- CENTER: search placeholder -->
<div class="page-header-center">
<div class="header-search-placeholder" aria-label="Search (coming soon)" role="search">
<svg class="header-search-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" width="16" height="16">
<circle cx="11" cy="11" r="8"/>
<path d="m21 21-4.35-4.35"/>
</svg>
<span class="header-search-text">Search...</span>
</div>
</div>
<!-- RIGHT: bell, inbox, avatar -->
<div class="page-header-right">
<!-- Bell placeholder -->
<button class="header-icon-button" aria-label="Notifications (coming soon)" type="button" disabled>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" width="18" height="18">
<path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9"/>
<path d="M10.3 21a1.94 1.94 0 0 0 3.4 0"/>
</svg>
</button>
<!-- Inbox placeholder -->
<button class="header-icon-button" aria-label="Inbox (coming soon)" type="button" disabled>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" width="18" height="18">
<polyline points="22 12 16 12 14 15 10 15 8 12 2 12"/>
<path d="M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z"/>
</svg>
</button>
<!-- Avatar dropdown using native details/summary -->
<details class="header-avatar-menu">
<summary class="header-avatar-button" aria-label="User menu">
<span class="header-avatar-initial">{ string([]rune(user.Email)[:1]) }</span>
</summary>
<div class="header-avatar-dropdown">
<!-- Workspace / user info -->
<div class="avatar-dropdown-workspace">
<span class="avatar-dropdown-workspace-name">{ user.Email }</span>
<span class="avatar-dropdown-workspace-meta">1 member</span>
</div>
<hr class="avatar-dropdown-divider"/>
<!-- Settings -->
<a href="/settings" class="avatar-dropdown-item">Settings</a>
<hr class="avatar-dropdown-divider"/>
<!-- Logout -->
<form method="POST" action="/logout" class="avatar-dropdown-logout-form">
@ui.CSRFField(csrfToken)
<button type="submit" class="avatar-dropdown-item avatar-dropdown-item--danger">Log out</button>
</form>
</div>
</details>
</div>
<!-- Per-page header actions slot (Phase 19-22 will populate) -->
if headerActions != nil {
<div class="page-header-actions">
@headerActions
</div>
}
</header>
Wire PageHeader into AppLayout. In the AppLayout body, inside the dashboard-main div, call PageHeader BEFORE { children... }:
<main id="app-main-content" class="dashboard-main">
@PageHeader(pageTitle, breadcrumb, headerActions, user, csrfToken)
{ children... }
</main>
Remove the earlier minimal breadcrumb stub that was added in Plan 01 (the `if len(breadcrumb) > 0 { <!-- comment --> }` and `if headerActions != nil { @headerActions }` blocks added as placeholders). The PageHeader call now handles both breadcrumb and headerActions rendering.
Add a small inline script at the bottom of the body (after existing script tags) for the avatar dropdown outside-click close behavior:
<script>
document.addEventListener('click', function(e) {
document.querySelectorAll('details.header-avatar-menu').forEach(function(d) {
if (!d.contains(e.target)) { d.removeAttribute('open'); }
});
});
</script>
</action>
<verify>
<automated>cd /Users/arthur.belleville/Documents/perso/projects/xtablo-source/backend && templ generate ./... && go build ./... 2>&1 | tail -5</automated>
</verify>
<done>PageHeader component exists in app_layout.templ. AppLayout calls @PageHeader inside dashboard-main before children. Build passes. templ generate produces no errors.</done>
</task>
<task type="auto">
<name>Task 2: Add page-header CSS to app.css, add /settings stub route, update tests</name>
<files>backend/internal/web/ui/app.css, backend/internal/web/router.go, backend/internal/web/handlers_tablos_test.go</files>
<action>
--- app.css: Add header bar CSS ---
Append a new section after the sidebar collapse rules added in Plan 02:
/* ── Page header bar ───────────────────────────────────────────────── */
.page-header {
display: flex;
align-items: center;
gap: 1rem;
padding: 0.75rem 1.5rem;
background: var(--color-surface-elevated);
border-bottom: 1px solid var(--color-border-panel);
margin: -2rem -2rem 1.5rem -2rem; /* cancel .dashboard-main padding on three sides, add bottom gap */
}
.page-header-left {
flex: 0 0 auto;
}
.page-header-center {
flex: 1 1 auto;
display: flex;
justify-content: center;
}
.page-header-right {
flex: 0 0 auto;
display: flex;
align-items: center;
gap: 0.5rem;
}
.page-header-actions {
flex: 0 0 auto;
margin-left: auto;
}
/* Breadcrumb */
.breadcrumb {
display: flex;
align-items: center;
gap: 0.375rem;
font-size: 0.875rem;
}
.breadcrumb-separator {
color: var(--color-text-muted);
user-select: none;
}
.breadcrumb-item {
color: var(--color-text-muted);
font-weight: 500;
}
.breadcrumb-item--link {
color: var(--color-text-brand);
text-decoration: none;
}
.breadcrumb-item--link:hover {
text-decoration: underline;
}
.breadcrumb-item--current {
color: var(--color-text-muted);
}
/* Search placeholder */
.header-search-placeholder {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.375rem 0.75rem;
border: 1px solid var(--color-border-panel);
border-radius: 0.375rem;
background: var(--color-surface-muted-inverse);
color: var(--color-text-muted);
font-size: 0.875rem;
min-width: 16rem;
max-width: 28rem;
width: 100%;
cursor: default;
}
/* Icon buttons (bell, inbox) */
.header-icon-button {
display: flex;
align-items: center;
justify-content: center;
width: 2rem;
height: 2rem;
border-radius: 0.375rem;
border: none;
background: transparent;
color: var(--color-text-muted);
cursor: pointer;
}
.header-icon-button:hover {
background: var(--overlay-dark-soft);
}
.header-icon-button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* Avatar dropdown */
.header-avatar-menu {
position: relative;
}
.header-avatar-menu > summary {
list-style: none;
cursor: pointer;
}
.header-avatar-menu > summary::-webkit-details-marker {
display: none;
}
.header-avatar-button {
display: flex;
align-items: center;
justify-content: center;
width: 2rem;
height: 2rem;
border-radius: 50%;
background: var(--color-text-brand);
color: #fff;
font-size: 0.8125rem;
font-weight: 600;
cursor: pointer;
border: none;
outline: none;
}
.header-avatar-initial {
text-transform: uppercase;
line-height: 1;
pointer-events: none;
}
.header-avatar-dropdown {
position: absolute;
right: 0;
top: calc(100% + 0.5rem);
min-width: 14rem;
background: var(--color-surface-elevated);
border: 1px solid var(--color-border-panel);
border-radius: 0.5rem;
box-shadow: var(--shadow-floating-control);
z-index: 50;
overflow: hidden;
}
.avatar-dropdown-workspace {
padding: 0.75rem 1rem;
display: flex;
flex-direction: column;
gap: 0.125rem;
}
.avatar-dropdown-workspace-name {
font-size: 0.875rem;
font-weight: 600;
color: var(--color-text-muted);
word-break: break-all;
}
.avatar-dropdown-workspace-meta {
font-size: 0.75rem;
color: var(--color-text-muted);
opacity: 0.7;
}
.avatar-dropdown-divider {
border: none;
border-top: 1px solid var(--color-border-panel-muted);
margin: 0;
}
.avatar-dropdown-item {
display: block;
width: 100%;
padding: 0.625rem 1rem;
font-size: 0.875rem;
color: var(--color-text-muted);
text-decoration: none;
background: transparent;
border: none;
text-align: left;
cursor: pointer;
}
.avatar-dropdown-item:hover {
background: var(--overlay-dark-soft);
}
.avatar-dropdown-item--danger {
color: #dc2626; /* red — logout action; no token for this; matches Figma red logout */
}
.avatar-dropdown-item--danger:hover {
background: #fef2f2;
}
.avatar-dropdown-logout-form {
display: contents; /* form element itself doesn't affect layout */
}
After adding these rules, regenerate tailwind.css:
cd backend && npx tailwindcss -i tailwind.input.css -o static/tailwind.css --minify
(Use the same CSS build command identified in Plan 02.)
--- router.go: Add /settings stub ---
In the authenticated routes block (inside the auth middleware group), add a GET handler for /settings that renders a simple stub page. Since no full settings template exists, use an inline http.HandlerFunc that writes a minimal HTML response:
r.Get("/settings", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
_, _ = w.Write([]byte(`<!DOCTYPE html><html><head><title>Settings - Xtablo</title><link rel="stylesheet" href="/static/tailwind.css"/></head><body style="padding:2rem;font-family:sans-serif"><h1>Settings</h1><p>Coming soon.</p><a href="/">Back to dashboard</a></body></html>`))
})
Place this route near the other single-page GET routes (e.g., next to /account/providers). This stub satisfies the D-06 requirement that the settings link navigates somewhere. A full settings page is deferred.
--- handlers_tablos_test.go: Update existing test and add header test ---
Locate TestTablosDashboard_Sidebar (around line 602). This test currently asserts "dashboard-sidebar" and "sidebar-nav-shell". These class names are preserved by the rebuild, so the test already passes. Add two new assertions to this test:
1. Check that "page-header" appears in the response body (the new header bar is present).
2. Check that "breadcrumb" appears in the response body.
Example using the existing assertion pattern:
if !strings.Contains(body, "page-header") {
t.Errorf("expected page-header class in response body")
}
if !strings.Contains(body, "breadcrumb") {
t.Errorf("expected breadcrumb class in response body")
}
Also add a new standalone test named TestTablosDashboard_Header that:
- Calls the TablosListHandler (same setup as TestTablosDashboard_Sidebar)
- Asserts the response body contains "page-header"
- Asserts the response body contains "header-avatar-menu" (the avatar dropdown element)
- Asserts the response body contains "Dashboard" (the breadcrumb label for the home page)
- Asserts the response body contains "header-search-placeholder"
Pattern: copy the setup from TestTablosDashboard_Sidebar — same mock setup, same request/response recorder pattern.
</action>
<verify>
<automated>cd /Users/arthur.belleville/Documents/perso/projects/xtablo-source/backend && grep -c "page-header" static/tailwind.css && go test ./internal/web/ -run "TestTablosDashboard" -v 2>&1 | tail -20</automated>
</verify>
<done>tailwind.css contains "page-header" CSS. TestTablosDashboard_Sidebar passes with new assertions. TestTablosDashboard_Header exists and passes. /settings route is registered and returns 200. Logout from avatar dropdown POSTs to /logout and succeeds.</done>
</task>
</tasks>
<threat_model>
## Trust Boundaries
| Boundary | Description |
|----------|-------------|
| Client → POST /logout | Logout is a state-changing POST; CSRF token guards against cross-site forgery |
| Avatar dropdown → DOM | Dropdown contains a logout form; clicking outside closes it via JS listener |
| user.Email → HTML | Email rendered inside dropdown; templ auto-escapes it |
## STRIDE Threat Register
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-18-03-01 | Cross-site request forgery | POST /logout from avatar dropdown | mitigate | ui.CSRFField(csrfToken) in the logout form — same pattern as the original SidebarOrganizationFooter logout form |
| T-18-03-02 | Tampering | user.Email rendered in dropdown | accept | templ auto-escapes all string interpolations; no raw HTML insertion possible |
| T-18-03-03 | Information disclosure | /settings stub exposes that settings page exists | accept | Page is behind authentication middleware; no sensitive data exposed |
| T-18-03-04 | Elevation of privilege | nil headerActions panic | mitigate | Plan 01 stub guard replaced by PageHeader's own nil-guard before @headerActions |
</threat_model>
<verification>
1. `cd backend && templ generate ./... && go build ./...` — zero errors
2. `grep -c "page-header" backend/static/tailwind.css` — returns > 0
3. `grep -c "header-avatar-menu" backend/static/tailwind.css` — returns > 0
4. `cd backend && go test ./... 2>&1 | tail -10` — all tests pass including TestTablosDashboard_Header
5. `cd backend && go test ./internal/web/ -run TestTablosDashboard_Header -v` — explicitly verify new test exists and passes
6. Manual: Start server, visit dashboard — top header bar visible with breadcrumb "Dashboard", search placeholder, bell and inbox placeholders, avatar circle
7. Manual: Click avatar circle — dropdown opens showing email, Settings link, Log out button (red text)
8. Manual: Click Log out — redirects to login page (POST /logout succeeds)
9. Manual: Visit a tablo detail page — breadcrumb shows "Dashboard > [tablo name]"
10. Manual: Click Settings in dropdown — navigates to /settings, shows "Coming soon" stub
</verification>
<success_criteria>
- PageHeader component renders in every authenticated page (verified by page-header class in test)
- Breadcrumb shows correct labels per page (Dashboard on home, Dashboard > title on tablo detail, Planning on /planning)
- Avatar dropdown uses details/summary HTML — no Alpine.js
- Avatar dropdown contains: email as workspace name, Settings link to /settings, Logout form with CSRF
- Logout POST succeeds from the avatar dropdown
- /settings route returns 200 with stub content
- TestTablosDashboard_Sidebar still passes (existing class names preserved)
- TestTablosDashboard_Header passes (new assertions for page-header, breadcrumb, header-avatar-menu)
- go build ./... and go test ./... both pass
- tailwind.css contains page-header and header-avatar-dropdown CSS
</success_criteria>
<output>
After completion, create `.planning/phases/18-app-shell-navigation/18-03-SUMMARY.md`
</output>

View file

@ -0,0 +1,148 @@
---
phase: 18-app-shell-navigation
verified: 2026-05-17T00:00:00Z
status: human_needed
score: 8/9 must-haves verified
overrides_applied: 0
human_verification:
- test: "Visit the dashboard at http://localhost:8080 and inspect the sidebar"
expected: "Sidebar shows 'General' section label above 5 nav items (Home, My Tasks, Projects, Events, Team Members), 'Projects' section label above tablo list, and collapse button that shrinks sidebar to icon-only on click"
why_human: "Visual Figma match requires rendering the browser; CSS collapse behavior is JavaScript-driven and cannot be verified without a live browser"
- test: "Click the avatar circle in the top header bar"
expected: "Dropdown opens showing user email, a Settings link, and a red Log out button; clicking outside closes it"
why_human: "Native details/summary open/close behavior and outside-click JS listener require live browser interaction"
- test: "Click Log out in the avatar dropdown"
expected: "POST /logout is submitted with CSRF token, session is destroyed, user is redirected to login"
why_human: "End-to-end logout flow requires a running server with an active session"
- test: "Navigate to a tablo detail page"
expected: "Breadcrumb reads 'Dashboard > [Tablo Title]', not just 'Dashboard'"
why_human: "Requires live server and a real tablo in the database to verify dynamic breadcrumb rendering"
- test: "Navigate to /settings"
expected: "Returns 200 with 'Coming soon' stub page and no 500 error"
why_human: "Requires a running server to verify the inline handler response"
---
# Phase 18: App Shell & Navigation Verification Report
**Phase Goal:** Redesign the sidebar and top header bar to match the Figma design.
**Verified:** 2026-05-17
**Status:** human_needed
**Re-verification:** No — initial verification
## Goal Achievement
### Observable Truths
| # | Truth | Status | Evidence |
|---|-------|--------|----------|
| 1 | The Go codebase compiles with zero errors after AppLayout signature is extended | VERIFIED | `go build ./...` exits 0 with no output |
| 2 | Every authenticated page still renders (no 500 errors) | UNCERTAIN (human) | Build passes; runtime smoke test requires live server |
| 3 | go test ./... passes with no failures | VERIFIED | All packages pass: internal/auth, internal/db, internal/files, internal/jobs, internal/web, internal/web/ui, templates |
| 4 | Sidebar shows a GENERAL section label above Home, My Tasks, Projects, Events, Team Members nav items | VERIFIED | `DashboardSidebar` in `app_layout.templ` lines 151-157: `sidebar-section-label` div with text "General", `sidebarPrimaryNavItems` returns exactly 5 items |
| 5 | Sidebar shows a PROJECTS section label above the tablo list | VERIFIED | `app_layout.templ` lines 161-177: inner `sidebar-section-label` div with text "Projects" above tablo loop |
| 6 | Chat and Files nav items are removed from the sidebar | VERIFIED | `sidebarPrimaryNavItems` in `app_layout_helpers.go` returns only: Home, My Tasks, Projects, Events, Team Members — no Chat or Files |
| 7 | Clicking the collapse button toggles the sidebar between full-width and icon-only states | UNCERTAIN (human) | Collapse button has correct inline `onclick` JS toggling `sidebar-is-collapsed` on `.dashboard-shell`; `tailwind.css` contains 5 occurrences of `sidebar-is-collapsed` CSS rules — requires browser to verify visual behavior |
| 8 | Every authenticated page shows a top header bar with breadcrumb, search placeholder, and right-side icons | VERIFIED | `PageHeader` component exists in `app_layout.templ` lines 186-267; called from `AppLayout` line 290 inside `.dashboard-main` before `{ children... }` |
| 9 | Breadcrumb renders the correct path for each page | VERIFIED | All 10 handler call sites in handlers_tablos.go, handlers_planning.go, handlers_account.go, handlers_discussion.go, handlers_files.go, handlers_events.go pass `[]templates.BreadcrumbItem` with correct labels and hrefs |
| 10 | Clicking the avatar circle opens a dropdown with workspace info, settings link, and logout button | UNCERTAIN (human) | `details.header-avatar-menu` with `summary` and dropdown div confirmed in `app_layout.templ` lines 237-257; outside-click JS listener at lines 298-302; requires live browser |
| 11 | Logout from the avatar dropdown still works — POST /logout succeeds | UNCERTAIN (human) | Logout `<form method="POST" action="/logout">` with `@ui.CSRFField(csrfToken)` is present in `PageHeader` (line 252-255); requires live server to verify end-to-end |
| 12 | Settings link in dropdown navigates to /settings which renders a stub page | VERIFIED | `/settings` route found at `router.go:90`; inline stub handler returning HTML with "Coming soon" content |
| 13 | go test ./... passes including updated sidebar test and new header test | VERIFIED | All packages pass; `TestTablosDashboard_Header` function confirmed at `handlers_tablos_test.go:647`; assertions for `page-header`, `header-avatar-menu`, `Dashboard`, `header-search-placeholder` all present |
**Score:** 8/9 automated truths verified (4 require human browser/server testing)
### Required Artifacts
| Artifact | Expected | Status | Details |
|----------|----------|--------|---------|
| `backend/templates/app_layout_helpers.go` | BreadcrumbItem struct export | VERIFIED | Lines 6-10: `type BreadcrumbItem struct { Label string; Href string }` |
| `backend/templates/app_layout_helpers.go` | sidebarPrimaryNavItems with Figma-spec items | VERIFIED | Lines 55-63: 5 items, "My Tasks" and "Team Members" present, Chat/Files absent |
| `backend/templates/app_layout.templ` | Extended AppLayout signature (8 params) | VERIFIED | Line 277: `templ AppLayout(title string, user *auth.User, csrfToken string, activePath string, tablos []sqlc.Tablo, pageTitle string, breadcrumb []BreadcrumbItem, headerActions templ.Component)` |
| `backend/templates/app_layout.templ` | DashboardSidebar with GENERAL/PROJECTS sections and collapse button | VERIFIED | Lines 128-181: sections present, inline onclick JS on collapse button at lines 139-147 |
| `backend/templates/app_layout.templ` | PageHeader component with three zones and avatar dropdown | VERIFIED | Lines 186-267: breadcrumb left, search center, bell/inbox/avatar right, details/summary dropdown |
| `backend/internal/web/ui/app.css` | is-collapsed CSS rules | VERIFIED | Lines 333-358: `.dashboard-shell.sidebar-is-collapsed` rules covering grid-template-columns, label hiding, icon centering, project-list hiding, chevron flip |
| `backend/internal/web/ui/app.css` | page-header CSS block | VERIFIED | Lines 362-567: complete `.page-header`, `.breadcrumb`, `.header-avatar-menu`, `.header-avatar-dropdown` CSS |
| `backend/internal/web/handlers_tablos_test.go` | TestTablosDashboard_Header test | VERIFIED | Lines 647-683: function exists with assertions for page-header, header-avatar-menu, Dashboard, header-search-placeholder |
| `backend/internal/web/router.go` | /settings stub route | VERIFIED | Line 90: `r.Get("/settings", ...)` inline handler |
### Key Link Verification
| From | To | Via | Status | Details |
|------|----|-----|--------|---------|
| `handlers_tablos.go` | `templates/tablos.templ` | `TablosDashboard / TabloDetailPage` with `BreadcrumbItem` args | VERIFIED | Grep confirms `[]templates.BreadcrumbItem{...}` at lines 62, 232, 350, 480 |
| `app_layout_helpers.go` | `app_layout.templ` | `sidebarPrimaryNavItems` called in `DashboardSidebar` | VERIFIED | `app_layout.templ` line 153: `for _, item := range sidebarPrimaryNavItems(activePath)` |
| `app_layout.templ` | `app.css` | `sidebar-is-collapsed` toggled by inline JS, CSS rule picks it up | VERIFIED | Both sides confirmed; CSS in app.css and tailwind.css (5 occurrences) |
| `app_layout.templ` | `app.css` | `.page-header` class rendered in `PageHeader`, CSS defines it | VERIFIED | `tailwind.css` has 5 occurrences of `page-header`; 3 occurrences of `header-avatar-menu` |
| `PageHeader` | `AppLayout` | `@PageHeader(...)` called inside `dashboard-main` | VERIFIED | `app_layout.templ` line 290: `@PageHeader(pageTitle, breadcrumb, headerActions, user, csrfToken)` |
| `app_layout.templ` | `/logout` route | `form method=POST action=/logout` with `@ui.CSRFField` | VERIFIED | `app_layout.templ` lines 252-255: form present with CSRF field |
| `handlers_planning.go` | `templates/planning.templ` | `PlanningPage` with breadcrumb args | VERIFIED | `handlers_planning.go:70` contains `[]templates.BreadcrumbItem{{Label: "Planning", Href: ""}}` |
| `handlers_account.go` | `templates/account_providers.templ` | `AccountProvidersPage` with breadcrumb args | VERIFIED | `handlers_account.go:51` contains `[]templates.BreadcrumbItem{{Label: "Linked Providers", Href: ""}}` |
| `handlers_discussion.go` | `templates/tablos.templ` | `TabloDetailPage` with breadcrumb args | VERIFIED | `handlers_discussion.go:82` contains `[]templates.BreadcrumbItem{{Label: "Dashboard", Href: "/"}, {Label: tablo.Title, Href: ""}}` |
| `handlers_files.go` | `templates/tablos.templ` | `TabloDetailPage` with breadcrumb args (2 call sites) | VERIFIED | `handlers_files.go:110,146` both contain breadcrumb args |
| `handlers_events.go` | `templates/tablos.templ` | `TabloDetailPage` with breadcrumb args | VERIFIED | `handlers_events.go:181` contains breadcrumb args |
### Requirements Coverage
| Requirement | Source Plan | Description | Status | Evidence |
|-------------|-------------|-------------|--------|----------|
| NAV-01 | 18-01, 18-02, 18-03 | User sees a redesigned sidebar/nav bar matching Figma (brand section, icon nav items, tablo list section, user footer) | VERIFIED (code) / NEEDS HUMAN (visual match) | `DashboardSidebar` rebuilt with two-section structure; brand/logo at lines 133-147; nav items at 153-157; tablo list at 164-177. SidebarOrganizationFooter moved to avatar dropdown in header — user info accessible via header not sidebar footer. Visual Figma match requires human. |
| NAV-02 | 18-01, 18-03 | User sees a per-page top header bar with page title and contextual action buttons matching Figma | VERIFIED (code) / NEEDS HUMAN (visual match) | `PageHeader` component at `app_layout.templ:186-267`; breadcrumb wired from all 10 handler call sites; `headerActions` slot present with nil-guard |
### Anti-Patterns Found
| File | Line | Pattern | Severity | Impact |
|------|------|---------|----------|--------|
| `backend/templates/app_layout.templ` | 109-122 | `SidebarOrganizationFooter` component defined but never called | Info | Dead code — intentional per Plan 02/03 decisions; safe to remove in a cleanup pass. Not a blocker. |
| `backend/templates/app_layout.templ` | 86-105 | `SidebarProjectsSection` component defined but never called | Info | Dead code — inlined directly into `DashboardSidebar` per Plan 02; safe to remove in cleanup. Not a blocker. |
No TBD, FIXME, or XXX markers found in phase-modified files.
### Human Verification Required
#### 1. Sidebar Visual Figma Match
**Test:** Start the server (`cd backend && go run . serve`), navigate to `http://localhost:8080`, inspect the sidebar.
**Expected:** Sidebar shows brand/logo section at top, "General" section label above 5 nav items (Home, My Tasks, Projects, Events, Team Members), "Projects" section label above the tablo list. Clicking the collapse button shrinks the sidebar to icon-only (~4rem) width and hides all text labels.
**Why human:** CSS collapse behavior is JavaScript-driven; the visual Figma match cannot be verified programmatically.
#### 2. Avatar Dropdown Interaction
**Test:** Click the user avatar circle in the top right of the header bar.
**Expected:** A dropdown opens showing the user email at the top, a "Settings" link, and a red "Log out" button. Clicking outside the dropdown closes it.
**Why human:** Native `<details>/<summary>` open/close behavior and the outside-click JS listener require live browser interaction.
#### 3. Logout Flow from Avatar Dropdown
**Test:** Click "Log out" in the avatar dropdown while authenticated.
**Expected:** A POST request is sent to `/logout` with a valid CSRF token, the session is destroyed, and the user is redirected to the login page.
**Why human:** Requires a running server with an active authenticated session.
#### 4. Tablo Detail Breadcrumb
**Test:** Navigate to a tablo detail page (`/tablos/{id}`).
**Expected:** The breadcrumb in the top header shows "Dashboard > [Tablo Title]" where "Dashboard" is a clickable link back to `/` and "[Tablo Title]" is plain text.
**Why human:** Requires a live server and a real tablo in the database to verify dynamic breadcrumb rendering.
#### 5. /settings Stub
**Test:** Navigate to `/settings` (authenticated).
**Expected:** Returns a 200 response with "Coming soon" text and a back link to the dashboard. No 500 error.
**Why human:** Requires a running server; the handler is an inline `http.HandlerFunc`, not a full template-rendered page.
### Gaps Summary
No gaps blocking goal achievement. All automated checks passed:
- `go build ./...` succeeds with zero errors
- `go test ./...` passes across all packages
- All 10 handler call sites supply `BreadcrumbItem` args
- `PageHeader` component exists and is wired into `AppLayout`
- `/settings` route is registered
- `TestTablosDashboard_Header` test exists with all required assertions
- Collapsed sidebar CSS rules are in both `app.css` and compiled `tailwind.css`
The 5 human verification items are runtime/visual checks that cannot be validated by static analysis alone. They are expected outcomes of the implementation — the code wiring is complete. Deferred dead code (`SidebarOrganizationFooter`, `SidebarProjectsSection` unused components) is informational only and does not block the phase goal.
---
_Verified: 2026-05-17_
_Verifier: Claude (gsd-verifier)_