# Phase 17: Chat & Planning - Pattern Map

**Mapped:** 2026-05-17
**Files analyzed:** 6 new/modified files
**Analogs found:** 6 / 6

## File Classification

| New/Modified File | Role | Data Flow | Closest Analog | Match Quality |
|---|---|---|---|---|
| `go-backend/internal/web/views/discussion_view.go` | model/view-helper | transform | `go-backend/internal/web/views/home.go` | exact |
| `go-backend/internal/web/views/planning_view.go` | model/view-helper | transform | `go-backend/internal/web/views/home.go` | exact |
| `go-backend/internal/web/views/dashboard_components.templ` | component | request-response | self (existing stubs being replaced) | exact |
| `go-backend/internal/web/views/discussion_view_test.go` | test | — | `go-backend/internal/web/views/dashboard_components_test.go` | exact |
| `go-backend/internal/web/views/planning_view_test.go` | test | — | `go-backend/internal/web/views/dashboard_components_test.go` | exact |
| `go-backend/internal/web/ui/app.css` | config/style | — | self (existing `.overview-section` block at line 875) | exact |
| `go-backend/internal/web/handlers/auth.go` | controller | request-response | self (existing `GetPlanningPage` / `GetChatPage` at lines 123–133) | exact |

---

## Pattern Assignments

### `go-backend/internal/web/views/discussion_view.go` (model, transform)

**Analog:** `go-backend/internal/web/views/home.go`

**Package and imports pattern** (lines 1–13):
```go
package views

import (
    "fmt"
    "strings"
    "time"

    tablomodel "xtablo-backend/internal/tablos"
    "xtablo-backend/internal/web/dates"
    "xtablo-backend/internal/web/tabloicons"

    "github.com/a-h/templ"
)
```
For `discussion_view.go` no external imports are needed beyond `package views` — the struct fields are all plain `string` and `bool`.

**Struct + constructor pattern** (home.go lines 61–68 for `dashboardTask`, adapted):
```go
type dashboardTask struct {
    Title      string
    Project    string
    ProjectKey string
    ProjectHue string
    Date       string
    Status     string
    StatusTone string
    Completed  bool
}
```
Apply same pattern: named struct with plain value fields, constructor returning a populated slice.

**Demo data factory pattern** (home.go lines 100–110):
```go
func overviewTasks() []dashboardTask {
    return []dashboardTask{
        {Title: "yo", Project: "Hello", ProjectKey: "H", ProjectHue: "blue", ...},
        ...
    }
}
```

**New file to create** — copy the struct + factory pattern:
```go
package views

type DiscussionMessageView struct {
    Author    string
    Timestamp string
    Body      string
    IsOwn     bool
}

type DiscussionTabData struct {
    Messages []DiscussionMessageView
}

func NewDiscussionTabData() DiscussionTabData {
    return DiscussionTabData{Messages: []DiscussionMessageView{
        // 3-5 hardcoded demo messages alternating IsOwn true/false
    }}
}
```

---

### `go-backend/internal/web/views/planning_view.go` (model, transform)

**Analog:** `go-backend/internal/web/views/home.go`

Same struct + factory pattern as `discussion_view.go`. Also contains the `PlanningShowDaySeparator` helper:

**Day separator helper pattern** (pure logic, no analog — implement fresh):
```go
func PlanningShowDaySeparator(events []PlanningEventRow, index int) bool {
    if index == 0 {
        return true
    }
    return events[index].DateLabel != events[index-1].DateLabel
}
```

**New file to create:**
```go
package views

type PlanningEventRow struct {
    DateLabel  string
    TimeRange  string
    Title      string
    TabloTitle string
    Location   string
}

type PlanningTabData struct {
    Events    []PlanningEventRow
    DateRange string
}

func NewPlanningTabData() PlanningTabData {
    return PlanningTabData{Events: []PlanningEventRow{
        // 3-5 hardcoded demo events spanning 2 dates
    }}
}

func PlanningShowDaySeparator(events []PlanningEventRow, index int) bool {
    if index == 0 {
        return true
    }
    return events[index].DateLabel != events[index-1].DateLabel
}
```

---

### `go-backend/internal/web/views/dashboard_components.templ` — `ChatMainContent` and `PlanningMainContent` stubs (component, request-response)

**Analog:** `OverviewProjectsSection` in `go-backend/internal/web/views/dashboard_components.templ` (lines 238–257)

**Overview-section heading pattern** (lines 238–256):
```go
templ OverviewProjectsSection(projects []TabloCardView) {
    <section id="overview-projects-section" class="overview-section">
        <div class="overview-section-heading">
            <h3>Mes Projets</h3>
        </div>
        <div class="project-grid">
            for _, project := range visibleOverviewProjects(projects) {
                @TabloGridCard(project)
            }
        </div>
        ...
    </section>
}
```
Planning heading uses the same `<section class="overview-section">` + `<div class="overview-section-heading">` wrapper. Replace `<h3>` with `<h1>` (requires extending the CSS selector — see Shared Patterns below). Add date-range label and nav buttons as right-side content inside `.overview-section-heading`.

**ui-card usage pattern** (card.css, lines 1–6):
```css
.ui-card {
  background: var(--color-surface-default);
  border: 1px solid var(--color-border-default);
  border-radius: 1rem;
  box-shadow: var(--shadow-surface-md);
}
```
Apply `class="ui-card"` to the `#discussion-messages` container div — no inline border/bg classes needed.

**EmptyState usage pattern** (`go-backend/internal/web/views/tablos.templ` lines 95–112):
```go
@ui.EmptyState(ui.EmptyStateProps{
    Title:       "Aucun projet trouvé",
    Description: "Créez votre premier projet",
    Icon:        ui.UIIcon("grid3x3"),
    Action: ui.Button(ui.ButtonProps{
        Label:   "Nouveau projet",
        Variant: ui.ButtonVariantDefault,
        Size:    ui.SizeMD,
        Type:    "button",
        Icon:    "plus",
        ...
    }),
})
```
For planning empty state: no `Action` needed — omit that field. For chat empty state: also no `Action`.

**Current stubs being replaced** (dashboard_components.templ lines 162–168):
```go
templ PlanningMainContent() {
    @AppSectionMainContent("Planning", "Visualisez le rythme...")
}

templ ChatMainContent() {
    @AppSectionMainContent("Discussions", "Retrouvez les conversations...")
}
```
Replace with real components accepting a data parameter:
```go
templ PlanningMainContent(data PlanningTabData) { ... }
templ ChatMainContent(data DiscussionTabData) { ... }
```

**For-range with conditional separator pattern** — closest analog is the tasks list in `OverviewTasks` (lines 318–336), but the day-separator variant is new. Use `for i, event := range data.Events` with `if PlanningShowDaySeparator(data.Events, i)` preceding each row.

---

### `go-backend/internal/web/handlers/auth.go` — `GetPlanningPage` and `GetChatPage` (controller, request-response)

**Analog:** `go-backend/internal/web/handlers/auth.go` lines 123–133 (the stubs being updated)

**renderAppPage helper pattern** (lines 178–206):
```go
func (h *AuthHandler) renderAppPage(activePath string, content func(user PublicUser) templ.Component) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        user, ok := h.authenticatedUser(r.Context(), r)
        if !ok {
            http.Redirect(w, r, "/login", http.StatusSeeOther)
            return
        }
        tablos, err := h.repo.ListTablos(r.Context(), ListTablosInput{OwnerID: user.ID})
        if err != nil {
            http.Error(w, "failed to load projects", http.StatusInternalServerError)
            return
        }
        w.Header().Set("Content-Type", "text/html; charset=utf-8")
        pageContent := content(user)
        var renderErr error
        if isHXRequest(r) {
            renderErr = views.DashboardContentSwap(activePath, tablos, pageContent).Render(r.Context(), w)
        } else {
            renderErr = views.DashboardPage(activePath, tablos, pageContent).Render(r.Context(), w)
        }
        if renderErr != nil {
            http.Error(w, "failed to render app page", http.StatusInternalServerError)
        }
    }
}
```

**Current call sites to update** (lines 123–133):
```go
func (h *AuthHandler) GetPlanningPage() http.HandlerFunc {
    return h.renderAppPage("/planning", func(user PublicUser) templ.Component {
        return views.PlanningMainContent()        // <-- add views.NewPlanningTabData()
    })
}

func (h *AuthHandler) GetChatPage() http.HandlerFunc {
    return h.renderAppPage("/chat", func(user PublicUser) templ.Component {
        return views.ChatMainContent()            // <-- add views.NewDiscussionTabData()
    })
}
```
After Phase 17 the `user PublicUser` parameter is available but not used — that is fine, consistent with how `GetFilesPage` and `GetFeedbackPage` also ignore the user (lines 135–143).

---

### `go-backend/internal/web/views/discussion_view_test.go` (test)

**Analog:** `go-backend/internal/web/views/dashboard_components_test.go`

**renderViewToString helper** (lines 143–151) — already defined in the same package; do NOT redefine it:
```go
func renderViewToString(t *testing.T, component templ.Component) string {
    t.Helper()
    var buf bytes.Buffer
    if err := component.Render(context.Background(), &buf); err != nil {
        t.Fatalf("render component: %v", err)
    }
    return buf.String()
}
```

**Render assertion pattern** (lines 44–65):
```go
func TestOverviewProjectsSectionRendersColorAndEditAction(t *testing.T) {
    record := tablomodel.Record{ ... }
    html := renderViewToString(t, OverviewProjectsSection(OverviewProjectsFromTablos([]tablomodel.Record{record})))
    for _, want := range []string{
        `style="--project-color:#22C55E;"`,
        `aria-label="Modifier le projet"`,
    } {
        if !strings.Contains(html, want) {
            t.Fatalf("expected %q in %q", want, html)
        }
    }
}
```

**Package declaration** (line 1): `package views` — same package as the components under test.

**Imports pattern** (lines 3–13):
```go
import (
    "bytes"
    "context"
    "strings"
    "testing"
    "time"

    "github.com/a-h/templ"
    "github.com/google/uuid"
    tablomodel "xtablo-backend/internal/tablos"
)
```
For discussion and planning tests, the minimal imports are: `"bytes"`, `"context"`, `"strings"`, `"testing"`.

---

### `go-backend/internal/web/views/planning_view_test.go` (test)

Same pattern as `discussion_view_test.go` above. Additionally needs a logic test for `PlanningShowDaySeparator`:

```go
func TestPlanningShowDaySeparator(t *testing.T) {
    events := []PlanningEventRow{
        {DateLabel: "May 17, 2026", ...},
        {DateLabel: "May 17, 2026", ...},
        {DateLabel: "May 18, 2026", ...},
    }
    // index 0 → true (always)
    // index 1 → false (same date as 0)
    // index 2 → true (date change)
}
```

---

### `go-backend/internal/web/ui/app.css` — new `.message-*` block and selector extension (config/style)

**Analog:** `.overview-section` block (app.css lines 875–892) — token-only CSS, no hardcoded hex

**Existing heading selector to extend** (lines 886–892):
```css
.overview-section-heading h3,
.tasks-section-header h3 {
  color: var(--color-surface-muted-inverse);
  font-size: 1.6rem;
  font-weight: 600;
  margin: 0;
}
```
Add `h1` to this rule:
```css
.overview-section-heading h1,
.overview-section-heading h3,
.tasks-section-header h3 { ... }
```

**New CSS block to append at end of file** (after line 1897):
```css
/* ── Message bubbles ── */

.message-row {
  padding: 0.75rem 1rem;
}

.message-own {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
}

.message-other {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
}

.message-bubble {
  border: 1px solid var(--color-border-subtle);
  border-radius: 0.25rem 0.75rem 0.75rem 0.75rem;
  max-width: 70%;
  padding: 0.75rem 1rem;
  white-space: pre-wrap;
  word-break: break-word;
}

.message-own .message-bubble {
  background: rgba(128, 78, 236, 0.10);
  border-radius: 0.75rem 0.75rem 0.25rem 0.75rem;
}

.message-other .message-bubble {
  background: var(--color-surface-default);
}

.message-meta {
  display: flex;
  gap: 0.5rem;
  margin-bottom: 0.25rem;
  font-size: 0.875rem;
  color: var(--color-text-muted);
}

.message-own .message-meta {
  justify-content: flex-end;
}

.message-meta .message-author {
  font-weight: 600;
}
```

---

## Shared Patterns

### Token-only CSS rule
**Source:** `go-backend/internal/web/ui/app.css` lines 875–892, `go-backend/internal/web/ui/base.css`
**Apply to:** All new `.message-*` rules in `app.css`

All `var(--...)` token names verified in `base.css`:
- `--color-brand-primary` (for rgba tint value `128, 78, 236`)
- `--color-surface-default`
- `--color-border-subtle`
- `--color-text-muted`
- `--color-surface-muted-inverse`
- `--shadow-surface-md`

Exception: the rgba brand tint `rgba(128, 78, 236, 0.10)` is spelled out intentionally (single-use value; wrapping in a token adds indirection without benefit).

### templ auto-escaping
**Source:** Project-wide convention; verified in all existing templ files
**Apply to:** All `{ variable }` interpolations in message templates (Author, Body, Timestamp)

Use `{ expr }` — never `templ.Raw()` for user-supplied or demo data fields.

### HTMX page swap pattern
**Source:** `go-backend/internal/web/handlers/auth.go` lines 196–204
**Apply to:** `GetPlanningPage` and `GetChatPage` handler updates

```go
if isHXRequest(r) {
    renderErr = views.DashboardContentSwap(activePath, tablos, pageContent).Render(r.Context(), w)
} else {
    renderErr = views.DashboardPage(activePath, tablos, pageContent).Render(r.Context(), w)
}
```
No changes to this branching logic — only the `content(user)` call site changes to pass a view model.

### Day separator visual style
**Source:** `CONTEXT.md` D-P03 (no existing analog to copy from — use inline Tailwind directly in the `.templ` file, consistent with other Tailwind-in-templ usage across the codebase):
```go
<div class="bg-slate-50 px-4 py-2 text-center text-sm text-slate-500">
    { event.DateLabel }
</div>
```
These Tailwind classes (`bg-slate-50`, `text-slate-500`) already exist in the generated `tailwind.css` (used by existing pages), so no purge issue.

---

## No Analog Found

All files have close analogs. No entries needed here.

---

## Metadata

**Analog search scope:** `go-backend/internal/web/views/`, `go-backend/internal/web/ui/`, `go-backend/internal/web/handlers/`
**Files scanned:** 7 (dashboard_components.templ, home.go, dashboard_components_test.go, auth.go, card.css, empty_state.templ, app.css)
**Pattern extraction date:** 2026-05-17
