10 KiB
| phase | slug | status | shadcn_initialized | preset | created |
|---|---|---|---|---|---|
| 17 | chat-planning | draft | false | none | 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-footerpadding: 20px 24px (1.25rem 1.5rem) — defined bycard.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
.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
.message-own {
display: flex;
flex-direction: column;
align-items: flex-end;
}
.message-other
.message-other {
display: flex;
flex-direction: column;
align-items: flex-start;
}
.message-bubble
.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
.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
.message-other .message-bubble {
background: var(--color-surface-default);
}
.message-meta
.message-meta {
display: flex;
gap: 0.5rem;
margin-bottom: 0.25rem;
font-size: 0.875rem;
color: var(--color-text-muted);
}
.message-own .message-meta
.message-own .message-meta {
justify-content: flex-end;
}
.message-meta .message-author
.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:
<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