---
phase: 17
slug: chat-planning
status: draft
shadcn_initialized: false
preset: none
created: 2026-05-17
---

# Phase 17 — UI Design Contract

> Visual and interaction contract for Phase 17: Chat & Planning.
> Generated by gsd-ui-researcher, verified by gsd-ui-checker.

---

## Design System

| Property | Value |
|----------|-------|
| Tool | none — custom CSS design system via `backend/internal/web/ui/` |
| Preset | not applicable |
| Component library | templ components + design-token CSS classes |
| Icon library | Heroicons (SVG, inline) |
| Font | ui-sans-serif / system-ui stack (declared in `base.css` body rule) |

Source: `backend/internal/web/ui/base.css`, `backend/internal/web/ui/app.css`, `backend/internal/web/ui/card.css`

---

## Spacing Scale

All values multiples of 4. Tokens are used in CSS via explicit `px`/`rem` values that map to this scale.

| Token | Value | Usage |
|-------|-------|-------|
| xs | 4px (0.25rem) | Icon gaps, inline tight padding |
| sm | 8px (0.5rem) | `.message-meta` gap, separator padding |
| md | 16px (1rem) | Default element padding (message bubble horizontal) |
| lg | 24px (1.5rem) | `.overview-section-heading` margin-bottom |
| xl | 32px (2rem) | `dashboard-main` padding |
| 2xl | 48px (3rem) | Major section breaks |
| 3xl | 64px (4rem) | Page-level spacing |

Exceptions:
- Message bubble vertical padding: 12px (0.75rem) — between sm and md, acceptable for chat density
- `.ui-card-header` / `.ui-card-body` / `.ui-card-footer` padding: 20px 24px (1.25rem 1.5rem) — defined by `card.css`, must not change
- Touch targets for tab nav items: 44px min-height — already declared in `.tab-nav-item`

---

## Typography

All sizes match existing system. No new sizes are introduced.

| Role | Size | Weight | Line Height | Token / Class |
|------|------|--------|-------------|---------------|
| Page heading (Planning h1) | 28px (1.75rem) | 600 | 1.25 | Matches `text-[28px] font-semibold` already in planning.templ |
| Section heading (Discussion h2) | 24px (1.5rem) | 600 | 1.25 | Matches `text-2xl font-semibold` already in discussion.templ |
| Body / message text | 16px (1rem) | 400 | 1.5 (leading-6) | `.message-bubble p` — inherits base |
| Label / meta (author, timestamp, date separator) | 14px (0.875rem) | 400 (author: 600) | 1.4 | `.message-meta` |

Source: `discussion.templ` existing type scale, `planning.templ` existing type scale — phase 17 must not introduce additional sizes.

---

## Color

All values use design tokens from `backend/internal/web/ui/base.css`. No hardcoded hex in new CSS.

| Role | Token | Resolved value | Usage |
|------|-------|----------------|-------|
| Dominant (60%) | `--color-surface-default` | `#ffffff` | Page background, `.ui-card` surface, others' message bubbles |
| Secondary (30%) | `--color-surface-subtle` / `--color-surface-muted` | `hsl(0 0% 96.1%)` / `#f3f4f6` | Day separators (`bg-slate-50` equivalent), event list backgrounds, composer border area |
| Accent (10%) | `--color-brand-primary` | `#804eec` | Own message bubble tint background only |
| Destructive | `--color-status-danger-strong` | `#dc2626` | Not applicable in this phase — no destructive actions |

**Accent reserved for:** Own message bubble background tint only (`rgba(128, 78, 236, 0.10)` — use the resolved value directly in `.message-own .message-bubble`). The brand accent must NOT appear on borders, nav items, or other phase 17 elements.

**Border tokens:**
- Message bubbles: `--color-border-subtle` (`#d0d5dd`) — 1px solid
- Discussion card container (`.ui-card`): `--color-border-default` (`hsl(0 0% 90.9%)`)
- Day separators: no explicit border — background differentiation only

**Text tokens:**
- Message body: `--color-text-primary` (`hsl(0 0% 9%)`)
- Meta (author email, timestamp, date separator): `--color-text-muted` (`hsl(0 0% 43.5%)`)
- Author email own message: `--color-text-brand` (`#804eec`) — weight 600
- Planning event title: `--color-surface-muted-inverse` (`#111827`)
- Planning event time/tablo label: `--color-text-secondary` (`#475467`)

---

## Component Inventory

### New CSS classes (all go into `backend/internal/web/ui/app.css`)

#### `.message-row`
```css
.message-row {
  padding: 0.75rem 1rem;
}
```
Flex container for a single message. No divider — drop `divide-y divide-slate-100` from the parent; bubbles carry their own visual weight.

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

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

#### `.message-bubble`
```css
.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-words;
}
```

#### `.message-own .message-bubble`
```css
.message-own .message-bubble {
  background: rgba(128, 78, 236, 0.10);
  border-radius: 0.75rem 0.75rem 0.25rem 0.75rem;
}
```
Own messages: top-right corner is tight (0.25rem) — classic outgoing bubble cue.

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

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

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

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

### Existing classes applied to new elements

| Element | Class | Source |
|---------|-------|--------|
| Discussion messages container `#discussion-messages` | `.ui-card` | `backend/internal/web/ui/card.css` |
| Planning page header wrapper | `.overview-section` | `backend/internal/web/ui/app.css` section 13 |
| Planning heading row | `.overview-section-heading` | `backend/internal/web/ui/app.css` section 13 |
| Planning empty state | `@ui.EmptyState(...)` | `backend/internal/web/ui/empty_state.templ` |

### Planning day separator element

Use the same visual approach as `DiscussionDaySeparator` for cross-surface consistency:

```html
<div class="bg-slate-50 px-4 py-2 text-center text-sm text-slate-500">
  { date label string }
</div>
```

Note: `bg-slate-50` and `text-slate-500` resolve to `--color-surface-muted` / `--color-text-muted` territory. This is an inline Tailwind usage that matches existing `DiscussionDaySeparator` exactly — acceptable because it mirrors an already-established pattern rather than introducing new values.

---

## Interaction Contract

### Discussion tab

| State | Behavior |
|-------|---------|
| Messages list — own | `.message-row.message-own` — bubble right-aligned, brand-tinted background, meta right-aligned |
| Messages list — others | `.message-row.message-other` — bubble left-aligned, white background, meta left-aligned |
| Day boundary | `DiscussionDaySeparator` renders above first message of each new day (existing logic unchanged) |
| Empty state | `DiscussionEmptyState` — existing templ component, no change |
| Composer submit | HTMX POST → `hx-swap="beforeend"` appends new `.message-row.message-own` row. New row must use `message.IsOwn = true` |
| SSE new message (others) | SSE stream appends `.message-row.message-other` row. `IsOwn = false` (default) |

### Planning page

| State | Behavior |
|-------|---------|
| Page heading | `.overview-section` > `.overview-section-heading` with h1 + date-range label, nav buttons right side |
| Events with multiple dates | `PlanningShowDaySeparator` helper — day separator rendered above first event of each new date |
| Events same date | No separator between them |
| Empty (no events) | `@ui.EmptyState(...)` — consistent with Phase 16 files section |
| Event row | TimeRange → Title → TabloTitle + Location (right column). `DateLabel` removed from row body |

---

## Copywriting Contract

| Element | Copy |
|---------|------|
| Primary CTA — discussion composer | "Send message" (existing, no change) |
| Discussion empty state heading | "No messages yet" (existing `DiscussionEmptyState`) |
| Discussion empty state body | "Start the discussion for this tablo." (existing) |
| Planning empty state heading | "No events in this range" (adopt via `@ui.EmptyState`) |
| Planning empty state body | "Use the navigation controls to browse another 14-day window." (adopt via `@ui.EmptyState`) |
| Planning nav — prev | "Previous 14 days" (existing, no change) |
| Planning nav — today | "Today" (existing, no change) |
| Planning nav — next | "Next 14 days" (existing, no change) |
| Error state | Not applicable — this phase has no form validation changes |
| Destructive confirmation | Not applicable — no destructive actions in scope |

Source: CONTEXT.md decisions + existing templ files. No copy changes required; existing strings are correct.

---

## Registry Safety

| Registry | Blocks Used | Safety Gate |
|----------|-------------|-------------|
| shadcn official | none | not required — project uses custom Go templ design system |
| third-party | none | not applicable |

No registry components. All UI is implemented via Go templ templates + CSS classes. No npm packages are installed.

---

## Acceptance Evidence Map

| Acceptance criterion | Spec ref | Visual contract |
|----------------------|----------|----------------|
| Own messages right-aligned, distinct background | REQ-1, REQ-2 | `.message-own` + `rgba(128,78,236,0.10)` tint |
| Others left-aligned | REQ-1 | `.message-other` default |
| SSE new message right-aligned for poster | REQ-2 | `message.IsOwn = true` → `.message-own` |
| Discussion container = `.ui-card` surface | REQ-3 | `.ui-card` on `#discussion-messages` |
| Planning heading = `.overview-section` | REQ-4 | `.overview-section` + `.overview-section-heading` |
| Events grouped under date separators | REQ-5 | `PlanningShowDaySeparator` + separator element |
| `go test ./...` exits 0 | REQ-6 | No new constraints from UI contract |

---

## Checker Sign-Off

- [ ] Dimension 1 Copywriting: PASS
- [ ] Dimension 2 Visuals: PASS
- [ ] Dimension 3 Color: PASS
- [ ] Dimension 4 Typography: PASS
- [ ] Dimension 5 Spacing: PASS
- [ ] Dimension 6 Registry Safety: PASS

**Approval:** pending
