---
phase: 15-dashboard-tablos
plan: 02
type: execute
wave: 1
depends_on:
  - 15-01
files_modified:
  - backend/internal/web/ui/app.css
  - backend/tailwind.input.css
  - backend/templates/app_layout.templ
  - backend/templates/app_layout_helpers.go
autonomous: true
requirements:
  - DASH-01
must_haves:
  truths:
    - "AppLayout renders a .dashboard-shell containing a .dashboard-sidebar and .dashboard-main"
    - "Sidebar contains brand logo (logo_dark.png), nav items with icons, SidebarProjectsSection, and SidebarOrganizationFooter"
    - "Active nav item receives is-active class based on activePath string"
    - "app.css is imported into tailwind.input.css and compiled into /static/tailwind.css"
  artifacts:
    - path: "backend/internal/web/ui/app.css"
      provides: "Sidebar and project-card CSS ported from go-backend"
      contains: ".dashboard-shell"
    - path: "backend/templates/app_layout.templ"
      provides: "AppLayout + DashboardSidebar + SidebarProjectsSection + SidebarOrganizationFooter templ components"
      exports: ["AppLayout", "DashboardSidebar", "SidebarProjectsSection", "SidebarOrganizationFooter"]
    - path: "backend/templates/app_layout_helpers.go"
      provides: "sidebarNavItemClass, isActivePath, sidebarNavItemID, sidebarNavItem type, sidebarPrimaryNavItems"
      contains: "func isActivePath"
  key_links:
    - from: "backend/tailwind.input.css"
      to: "backend/internal/web/ui/app.css"
      via: "@import"
      pattern: "@import.*web/ui/app.css"
    - from: "backend/templates/app_layout.templ"
      to: "backend/templates/app_layout_helpers.go"
      via: "sidebarPrimaryNavItems + sidebarNavItemClass calls"
      pattern: "sidebarPrimaryNavItems"
---

## Phase Goal

**As a** signed-in user, **I want to** see a sidebar-based dashboard with project cards for my tablos, **so that** the app matches the go-backend visual design and I can navigate my work efficiently.

<objective>
Create the AppLayout shell and its CSS foundation. This plan delivers two things: (1) the new `backend/internal/web/ui/app.css` file with sidebar and project-card CSS ported from go-backend, registered in tailwind.input.css, and (2) the `backend/templates/app_layout.templ` file with AppLayout + sidebar sub-components + helpers.

No handler or tablos.templ changes in this plan — that is Plan 03.

Purpose: AppLayout is the foundation all authenticated pages will use. The CSS and component must exist before tablos.templ and handler call sites can be updated.

Output: app.css (new), tailwind.input.css (updated), app_layout.templ (new), app_layout_helpers.go (new).
</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>
@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.planning/ROADMAP.md
@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.planning/phases/15-dashboard-tablos/15-CONTEXT.md
@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.planning/phases/15-dashboard-tablos/15-RESEARCH.md
@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.planning/phases/15-dashboard-tablos/15-PATTERNS.md

<interfaces>
<!-- Key types and contracts the executor needs. -->

From backend/templates/auth_layout.templ (pattern to follow):
```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>
}
```

From backend/internal/web/ui/icon_button.templ — UIIcon accepts these case strings:
"pencil", "trash", "plus", "grid3x3", "list", "filter", "search", "calendar"
-- "panels", "layers", "planning", "chat", "files", "tasks" are NOT yet in UIIcon

From go-backend/internal/web/views/icons.templ — SidebarIcon implements:
"panels" (rect+paths), "tasks" (rect+paths), "layers" (paths), "planning" (rect+paths), "chat" (path), "files" (path)

From backend/tailwind.input.css (current end of file):
@import "./internal/web/ui/spacing.css";
-- app.css must be appended after this line

From backend/internal/web/ui/variants.go — IconButtonClass output:
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"

AppLayout signature (per D-L01):
templ AppLayout(title string, user *auth.User, csrfToken string, activePath string, tablos []sqlc.Tablo)

Sub-components:
templ DashboardSidebar(activePath string, tablos []sqlc.Tablo, user *auth.User, csrfToken string)
templ SidebarProjectsSection(tablos []sqlc.Tablo)
templ SidebarOrganizationFooter(user *auth.User, csrfToken string)
</interfaces>
</context>

<tasks>

<task type="auto">
  <name>Task 1: Port sidebar + project-card CSS into app.css and register in tailwind</name>
  <files>backend/internal/web/ui/app.css, backend/tailwind.input.css</files>

  <read_first>
    - go-backend/internal/web/ui/app.css — read lines 455–743 (dashboard shell, sidebar), then lines 875–892 (overview section heading), then lines 894–1046 (project-card). Port these sections verbatim. All color values already use var(--...) tokens — do not substitute.
    - backend/internal/web/ui/auth.css — read to understand the file structure pattern (single-purpose CSS file, no Tailwind directives, uses var(--...) tokens from base.css).
    - backend/tailwind.input.css — read to confirm current last line and the exact @import path format used.
    - backend/internal/web/ui/base.css — skim to confirm which CSS custom property names exist (color tokens, spacing tokens) so any go-backend vars not found in base.css can be noted.
  </read_first>

  <action>
    Create backend/internal/web/ui/app.css. The file contains CSS ported verbatim from go-backend/internal/web/ui/app.css for these class groups only (in order):

    1. .dashboard-shell, .dashboard-sidebar (grid layout — lines 455–465 in go-backend)
    2. .sidebar-nav-shell (lines 467–479)
    3. .sidebar-brand, .sidebar-brand-link, .sidebar-brand-logo, .sidebar-brand-title (lines 481–509)
    4. .sidebar-collapse-button (lines 511–527 — include the CSS; the button is non-functional without JS in Phase 15)
    5. .sidebar-primary, .sidebar-list, .sidebar-divider (lines 533–558)
    6. .sidebar-nav-item, .sidebar-nav-item:hover, .sidebar-nav-item.is-active (lines 560–578)
    7. .sidebar-nav-link, .sidebar-nav-link-inner, .sidebar-nav-icon, .sidebar-nav-label (lines 580–605)
    8. .sidebar-projects, .sidebar-section-label, .sidebar-project-list (lines 607–628)
    9. .sidebar-project-link, .sidebar-project-icon, .sidebar-project-label (lines 630–668)
    10. .sidebar-footer-links (lines 670–673)
    11. .sidebar-organization, .organization-button, .organization-avatar, .organization-copy, .organization-name, .organization-meta (lines 675–732 — per D-F03; include .organization-copy if it exists)
    12. .dashboard-main (lines 734–741)
    13. .overview-section, .overview-section-heading (lines 875–892)
    14. .project-grid (lines 894–899)
    15. .project-card, .project-card-top (lines 900–944)
    16. Project card icon button overrides — adapt go-backend 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); }`
        Note: do NOT use `.borderless-icon-button` as a selector for these overrides if go-backend uses a different class — use the VERIFIED class names from variants.go output above.
    17. .project-card-title-row, .project-avatar (lines 986–1005)
    18. .project-date-row (lines 1039–1046)

    Add one line to backend/tailwind.input.css after the last existing @import line:
    `@import "./internal/web/ui/app.css";`
  </action>

  <verify>
    <automated>cd /Users/arthur.belleville/Documents/perso/projects/xtablo-source/backend && grep -c "dashboard-shell" internal/web/ui/app.css && grep -c "project-card" internal/web/ui/app.css && grep -c "app.css" tailwind.input.css</automated>
  </verify>

  <acceptance_criteria>
    - `backend/internal/web/ui/app.css` exists and contains `.dashboard-shell`: `grep -c "\.dashboard-shell" backend/internal/web/ui/app.css` returns >= 1
    - File contains `.project-card`: `grep -c "\.project-card" backend/internal/web/ui/app.css` returns >= 1
    - File contains `.sidebar-organization`: `grep -c "\.sidebar-organization" backend/internal/web/ui/app.css` returns >= 1
    - No hard-coded hex colors: `grep -v "var(--" backend/internal/web/ui/app.css | grep -cE "#[0-9a-fA-F]{3,6}"` returns 0 (all colors use design tokens)
    - `backend/tailwind.input.css` imports app.css: `grep -c "web/ui/app.css" backend/tailwind.input.css` returns 1
    - `go build ./...` from backend/ exits 0 (CSS changes do not affect Go compilation)
  </acceptance_criteria>

  <done>app.css exists with all sidebar + project-card CSS classes. tailwind.input.css registers the import. go build exits 0.</done>
</task>

<task type="auto">
  <name>Task 2: Create app_layout_helpers.go and app_layout.templ with all sidebar sub-components</name>
  <files>backend/templates/app_layout_helpers.go, backend/templates/app_layout.templ</files>

  <read_first>
    - backend/templates/auth_layout.templ — read the full file for the exact package declaration, import style, and HTML shell pattern to replicate.
    - backend/templates/layout.templ — read lines 40–60 for the three script tags that must be preserved in AppLayout (htmx.min.js, sortable.min.js, discussion-sse.js per D-L04).
    - go-backend/internal/web/views/dashboard_components.templ — read lines 54–145 for DashboardSidebar, SidebarNavItem, SidebarProjectsSection, SidebarOrganization component structure and logic.
    - go-backend/internal/web/views/home.go — read lines 1–170 for sidebarNavItemClass, isActivePath, sidebarNavItemID, sidebarNavItem struct, and sidebarPrimaryNavItems slice builder.
    - go-backend/internal/web/views/icons.templ — read lines 80–130 for SidebarIcon SVG content for: "panels", "tasks", "layers", "planning", "chat", "files".
    - backend/internal/web/ui/icon_button.templ — read UIIcon switch cases to confirm which icons already exist (pencil, trash, etc.) vs. which are sidebar-only.
    - backend/templates/tablos.templ — read line 98 for the templ.SafeURL pattern used in href attributes.
  </read_first>

  <action>
    Create backend/templates/app_layout_helpers.go in package templates. This is a pure Go file (no templ directives). It contains:

    1. Import: "strings" only.
    2. sidebarNavItem struct with fields: Href string, Label string, Icon string, Active bool, DividerAfter bool.
    3. func sidebarNavItemClass(active bool) string — returns "sidebar-nav-item is-active" when active, "sidebar-nav-item" otherwise.
    4. func isActivePath(activePath string, href string) bool — returns strings.TrimSpace(activePath) != "" && activePath == href.
    5. func sidebarNavItemID(href string) string — switch on href, case "/": return "sidebar-nav-home", default: slug from strings.Trim(strings.ReplaceAll(href, "/", "-"), "-"), prefix with "sidebar-nav-".
    6. func sidebarPrimaryNavItems(activePath string) []sidebarNavItem — return the slice per D-N01/D-N02:
       - {Href: "/", Label: "Dashboard", Icon: "panels", Active: isActivePath(activePath, "/"), DividerAfter: true}
       - {Href: "", Label: "Tasks", Icon: "tasks", Active: false} (visual-only per D-N02, no href)
       - {Href: "/planning", Label: "Planning", Icon: "planning", Active: isActivePath(activePath, "/planning")}
       - {Href: "", Label: "Chat", Icon: "chat", Active: false} (visual-only per D-N03)
       - {Href: "", Label: "Files", Icon: "files", Active: false} (visual-only per D-N02)

    Create backend/templates/app_layout.templ in package templates. It contains these components in order:

    A. templ SidebarNavIcon(kind string) — a switch/case block with the SVG content for each icon kind: "panels", "tasks", "layers", "planning", "chat", "files". Copy SVG paths verbatim from go-backend/internal/web/views/icons.templ SidebarIcon. Default case renders an empty span. This avoids extending UIIcon in the ui package for sidebar-only icons.

    B. templ SidebarNavItemRow(item sidebarNavItem) — renders one nav list item. If item.Href is empty, render as a div with class sidebarNavItemClass(item.Active) and style="cursor: default" (no anchor tag, per D-N02). If item.Href is non-empty, render as an anchor tag with href={templ.SafeURL(item.Href)} and class sidebarNavItemClass(item.Active). Inside both cases, render the .sidebar-nav-link-inner div with .sidebar-nav-icon span containing @SidebarNavIcon(item.Icon) and .sidebar-nav-label div containing item.Label.

    C. templ SidebarProjectsSection(tablos []sqlc.Tablo) — per the pattern in PATTERNS.md: div.sidebar-projects > hr[role=separator] > div.sidebar-section-label "Projects" > ul.sidebar-project-list. Each tablo renders as li > a.sidebar-project-link with href=templ.SafeURL("/tablos/"+tablo.ID.String()), containing span.sidebar-project-icon (with style="background-color: "+tablo.Color.String if tablo.Color.Valid && tablo.Color.String != "") and span.sidebar-project-label containing tablo.Title.

    D. templ SidebarOrganizationFooter(user *auth.User, csrfToken string) — per D-F01/D-F02/D-F03: div.sidebar-organization > div.organization-button > span.organization-avatar containing the first rune of user.Email (use string([]rune(user.Email)[:1])) > span.organization-copy > span.organization-name containing user.Email > form[method=POST][action=/logout] with @ui.CSRFField(csrfToken) and a submit button with text "Log out" and class "text-sm".

    E. templ DashboardSidebar(activePath string, tablos []sqlc.Tablo, user *auth.User, csrfToken string) — aside.dashboard-sidebar > nav[aria-label="Main navigation"].sidebar-nav-shell > div.sidebar-brand with a.sidebar-brand-link[href=/][aria-label="Home"] containing img.sidebar-brand-logo[src=/static/logo_dark.png][alt="Logo XTablo"] and h1.sidebar-brand-title "XTablo" > div.sidebar-primary > ul.sidebar-list[role=list] looping sidebarPrimaryNavItems(activePath) rendering @SidebarNavItemRow(item) for each > @SidebarProjectsSection(tablos) > @SidebarOrganizationFooter(user, csrfToken).

    F. templ AppLayout(title string, user *auth.User, csrfToken string, activePath string, tablos []sqlc.Tablo) — full HTML shell following AuthLayout pattern: DOCTYPE html > html[lang=en] > head (charset, viewport, title, link[rel=stylesheet][href=/static/tailwind.css]) > body > div.dashboard-shell > @DashboardSidebar(activePath, tablos, user, csrfToken) > main#app-main-content.dashboard-main > { children... } > end div > three script tags with defer: /static/htmx.min.js, /static/sortable.min.js, /static/discussion-sse.js (per D-L04).

    Imports for app_layout.templ: "backend/internal/auth", "backend/internal/db/sqlc", "backend/internal/web/ui".
  </action>

  <verify>
    <automated>cd /Users/arthur.belleville/Documents/perso/projects/xtablo-source/backend && templ generate ./... 2>&amp;1 &amp;&amp; go build ./... 2>&amp;1</automated>
  </verify>

  <acceptance_criteria>
    - app_layout_helpers.go compiles in package templates: `go build ./...` exits 0
    - app_layout.templ generates without error: `templ generate ./...` exits 0
    - app_layout.templ contains func AppLayout: `grep -c "templ AppLayout(" backend/templates/app_layout.templ` returns 1
    - app_layout.templ contains func DashboardSidebar: `grep -c "templ DashboardSidebar(" backend/templates/app_layout.templ` returns 1
    - app_layout.templ contains func SidebarProjectsSection: `grep -c "templ SidebarProjectsSection(" backend/templates/app_layout.templ` returns 1
    - app_layout.templ contains func SidebarOrganizationFooter: `grep -c "templ SidebarOrganizationFooter(" backend/templates/app_layout.templ` returns 1
    - app_layout_helpers.go contains func isActivePath: `grep -c "func isActivePath" backend/templates/app_layout_helpers.go` returns 1
    - app_layout_helpers.go contains func sidebarPrimaryNavItems: `grep -c "func sidebarPrimaryNavItems" backend/templates/app_layout_helpers.go` returns 1
    - `go build ./...` from backend/ exits 0 — no compile errors
    - `go test ./... -count=1` exits 0 (skips without DB; existing tests not broken)
  </acceptance_criteria>

  <done>AppLayout and all sidebar sub-components exist and compile. isActivePath and sidebarPrimaryNavItems helpers are in place. No handler or tablos.templ changes yet — that is Plan 03.</done>
</task>

</tasks>

<threat_model>
## Trust Boundaries

| Boundary | Description |
|----------|-------------|
| template → browser | HTML rendered server-side; CSS served from /static/tailwind.css |
| user.Email → sidebar footer | Email displayed as-is; templ auto-escapes HTML entities |

## STRIDE Threat Register

| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-15-02-01 | Information Disclosure | SidebarOrganizationFooter | accept | user.Email already visible to the authenticated user; templ escapes HTML |
| T-15-02-02 | Spoofing | sidebar nav hrefs | accept | Nav hrefs are hardcoded strings, not user input; templ.SafeURL guards dynamic tablo hrefs |
</threat_model>

<verification>
`cd /Users/arthur.belleville/Documents/perso/projects/xtablo-source/backend && templ generate ./... && go build ./... && go test ./... -count=1`

All existing tests pass (skip without DB). New files compile. app.css registered in tailwind.input.css.
</verification>

<success_criteria>
- `backend/internal/web/ui/app.css` exists with sidebar + project-card CSS
- `backend/tailwind.input.css` imports app.css
- `backend/templates/app_layout.templ` exists with AppLayout + DashboardSidebar + SidebarProjectsSection + SidebarOrganizationFooter
- `backend/templates/app_layout_helpers.go` exists with isActivePath + sidebarPrimaryNavItems
- `templ generate ./... && go build ./...` exits 0
</success_criteria>

<output>
After completion, create `.planning/phases/15-dashboard-tablos/15-02-SUMMARY.md`
</output>
