docs(17): add UI design contract for chat & planning phase

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Arthur Belleville 2026-05-17 06:30:40 +02:00
parent 08e8fc1976
commit b84503a54a
No known key found for this signature in database

View file

@ -0,0 +1,278 @@
---
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