[nexus] docs(21): UI design contract

This commit is contained in:
Mikkel Georgsen 2026-04-01 12:36:12 +02:00
parent 72764b1e93
commit 181af7b00d

View file

@ -0,0 +1,273 @@
---
phase: 21
slug: chat-foundation
status: draft
shadcn_initialized: true
preset: new-york / neutral / cssVariables
created: 2026-04-01
---
# Phase 21 — UI Design Contract
> Visual and interaction contract for frontend phases. Generated by gsd-ui-researcher, verified by gsd-ui-checker.
---
## Design System
| Property | Value |
|----------|-------|
| Tool | shadcn (new-york style) |
| Preset | new-york, baseColor: neutral, cssVariables: true |
| Component library | Radix UI (via shadcn) |
| Icon library | lucide-react (^0.574.0) |
| Font | System UI stack (inherited from existing CSS — no custom font declared) |
Source: `ui/components.json`, `ui/src/index.css`
**shadcn components already installed (usable without install):**
`Avatar`, `Badge`, `Button`, `Card`, `Checkbox`, `Dialog`, `DropdownMenu`, `Input`, `ScrollArea`, `Separator`, `Sheet`, `Skeleton`, `Tabs`, `Textarea`, `Tooltip`
**New components for this phase (install before use):**
None required — all necessary primitives are available. `Textarea` covers `ChatInput`, `ScrollArea` covers the message list and conversation list.
---
## Spacing Scale
Declared values (multiples of 4 only). Source: 8-point scale, confirmed via existing `p-4 md:p-6` usage in `Layout.tsx`.
| Token | Value | Usage |
|-------|-------|-------|
| xs | 4px | Icon gaps (`gap-1`), inline icon + label spacing |
| sm | 8px | Compact padding inside list items (`px-2 py-1`), badge padding |
| md | 16px | Default element padding (`p-4`), conversation list item padding |
| lg | 24px | Section padding on desktop (`p-6`), chat panel header padding |
| xl | 32px | Gap between major UI zones |
| 2xl | 48px | Empty-state vertical padding (`py-12`) |
| 3xl | 64px | Page-level section breaks (not applicable in panel context) |
Exceptions:
- Touch targets on coarse-pointer devices: `min-height: 44px` (already enforced globally in `index.css` `@media (pointer: coarse)`)
- Sidebar conversation list item: 12px vertical padding (`py-3`) — between sm and md — acceptable as it follows existing `EntityRow` pattern
- Chat input bottom padding: `pb-[calc(env(safe-area-inset-bottom)+16px)]` on mobile to clear the home indicator
---
## Typography
All sizes use Tailwind utility classes mapped to the project's system-UI font stack. Source: observed usage in `Layout.tsx`, `EmptyState.tsx`, `MarkdownBody.tsx`.
| Role | Size | Weight | Line Height | Tailwind Class |
|------|------|--------|-------------|----------------|
| Body | 14px | 400 (regular) | 1.5 | `text-sm` |
| Label | 13px | 500 (medium) | 1.4 | `text-[13px] font-medium` |
| Heading | 16px | 600 (semibold) | 1.25 | `text-base font-semibold` |
| Meta / Timestamp | 12px | 400 (regular) | 1.4 | `text-xs text-muted-foreground` |
Rules:
- Conversation titles in the sidebar use Label (13px / medium).
- Agent message content renders inside `MarkdownBody` which applies `prose prose-sm` — do not override prose typography for markdown content.
- The chat input `Textarea` uses body (14px / regular).
- Section headers inside the chat panel (e.g. "Conversations") use Heading (16px / semibold).
- Timestamps and message count badges use Meta (12px / regular / muted-foreground).
---
## Color
All colors reference CSS custom properties already declared for all three themes (Catppuccin Mocha, Tokyo Night, Catppuccin Latte) in `ui/src/index.css`. Never hard-code hex values. Source: `ui/src/index.css`.
| Role | CSS Variable | 60/30/10 | Usage |
|------|-------------|----------|-------|
| Dominant surface | `--background` | 60% | Chat panel background, message list background, main layout background |
| Secondary surface | `--card` / `--sidebar` | 30% | Conversation list sidebar background (`--sidebar`), individual conversation list item hover state, code block container |
| Accent | `--primary` | 10% | Send button, active conversation highlight border-left indicator, conversation pin icon (filled) |
| Muted | `--muted` | — | Input background, empty state icon container |
| Muted foreground | `--muted-foreground` | — | Placeholder text in chat input, timestamps, secondary labels |
| Destructive | `--destructive` | — | Delete conversation action only |
| Border | `--border` | — | Dividers between message groups, chat panel border, input border |
Accent (`--primary`) is reserved for exactly these elements:
1. The "Send" / "New conversation" primary action button background
2. The active conversation in the sidebar (left-border indicator: `border-l-2 border-primary`)
3. Filled pin icon on pinned conversations
The accent must NOT be applied to: conversation list hover states, timestamps, agent message bubbles, or any decorative elements.
Theme-specific code highlighting:
- `catppuccin-mocha` (dark): use highlight.js theme `base16/catppuccin` (dark variant via `.dark` class)
- `tokyo-night` (dark): use highlight.js theme `tokyo-night-dark` (via `.theme-tokyo-night.dark` class)
- `catppuccin-latte` (light): use highlight.js theme `base16/catppuccin` (light variant via `:root` / light mode class)
Implementation: Load one CSS file with per-theme class overrides for `.hljs` variables rather than three separate `<link>` imports. See RESEARCH.md Pitfall 1.
---
## Component Inventory
Components to build for this phase. Each references the design token contract above.
### ChatPanel (right-side drawer)
- Width: 380px open, 0px closed
- Transition: `transition-[width] duration-100 ease-out` (same as sidebar in `Layout.tsx`)
- Background: `bg-background` (dominant surface)
- Position: sibling of `<PropertiesPanel>` in the Layout flex row; opening ChatPanel closes PropertiesPanel (see RESEARCH.md Open Question 1 resolution)
- Header: 48px tall, `border-b border-border`, contains "Chat" title (Heading) + "New conversation" icon button (Plus from lucide) + close icon button
- localStorage key: `nexus:chat-panel-open` (use `nexus:` prefix, not `paperclip:`)
### ChatConversationList (inside ChatPanel left column, width: 240px)
- Background: `bg-sidebar` (secondary surface)
- Item height: 48px (`py-3 px-3`)
- Active item: `border-l-2 border-primary bg-sidebar-accent`
- Hover item: `bg-sidebar-accent/50`
- Title: Label (13px / medium), truncated with `truncate`
- Timestamp: Meta (12px / muted-foreground), right-aligned
- Pin icon: 14px, `text-primary` when active
- Archive icon: 14px, `text-muted-foreground`
- Skeleton loader: 3 skeleton items (`<Skeleton>`) while `isLoading`
- Infinite scroll sentinel: `<div ref={sentinelRef}>` at bottom of list
### ChatMessageList (main chat area)
- Background: `bg-background`
- Padding: `p-4` with `gap-4` between messages
- User message: right-aligned, `bg-secondary text-secondary-foreground`, `rounded-none` (matches `--radius: 0` global setting), `max-w-[75%]`, padding `px-4 py-2`
- Assistant message: left-aligned, no background (transparent), `max-w-[85%]`
- Message timestamp: Meta, visible on hover only (`opacity-0 group-hover:opacity-100 transition-opacity`)
- Agent label on assistant messages: not in Phase 21 (agent identity lands in Phase 22) — omit avatar/name row entirely
### ChatMarkdownMessage (assistant message renderer)
- Extends `MarkdownBody` with `rehype-highlight` added to `rehypePlugins`
- Code block container: `bg-card border border-border`, `relative` positioning for copy button
- Code block language label: Meta (12px), `text-muted-foreground`, top-left inside block (`absolute top-2 left-3`)
- Copy button: icon-only (Copy from lucide, 14px), `absolute top-1.5 right-1.5`, `variant="ghost" size="icon-sm"`, transitions to Check icon for 2 seconds on success
- Copy button accessible label: `aria-label="Copy code"`
### ChatInput (bottom of ChatPanel)
- Component: `<Textarea>` (shadcn) with `field-sizing: content` CSS + scroll-height fallback
- Min height: 40px (one line)
- Max height: 160px (approx 6 lines), then scrolls internally
- Padding: `px-3 py-2.5`
- Background: `bg-muted` (matches `--muted` for input feel)
- Border: `border border-border focus:ring-1 focus:ring-ring`
- Placeholder: "Send a message..." — `text-muted-foreground`
- Send button: positioned to the right of the textarea (flex row), `variant="default"`, icon-only (Send from lucide, 16px), disabled when input is empty
- Keyboard shortcuts:
- `Enter` (without Shift) → submit
- `Shift+Enter` → newline
- `Escape` → clear input (if input has content) OR close ChatPanel (if input is empty)
---
## Interaction Contract
### Conversation CRUD actions
Actions are available via a `<DropdownMenu>` (lucide `MoreHorizontal` icon) that appears on hover over a conversation list item.
| Action | Icon | Color | Confirmation |
|--------|------|-------|-------------|
| Pin / Unpin | Pin / PinOff | default foreground | None — immediate |
| Archive | Archive | default foreground | None — immediate |
| Delete | Trash2 | `text-destructive` | Inline confirm: replace DropdownMenu trigger with "Delete?" + "Yes" / "No" buttons inside the same popover |
No separate Dialog for delete — use inline confirmation popover to keep the interaction contained in the sidebar.
### Conversation title editing
- Trigger: double-click on the conversation title in the list, OR via a "Rename" item in the DropdownMenu
- Inline input replaces the title text (`<input>` at same 13px font size)
- Confirm: Enter or blur
- Cancel: Escape restores previous title
### Sidebar infinite scroll
- Load more trigger: `IntersectionObserver` on a sentinel div at the bottom of the conversation list
- While loading next page: show 2 `<Skeleton>` items at the bottom
- End of list: no visual indicator (list simply ends)
### State: sending a message
- Send button becomes `disabled` and shows a `Loader2` spin icon while the POST is in flight
- Input is disabled during submission
- On success: input clears, message appears at bottom of `ChatMessageList`
- On error: input re-enables, toast notification fires (uses existing `ToastViewport`)
---
## Copywriting Contract
Source: Claude's discretion (discuss phase skipped). All copy follows Nexus tone: direct, no corporate language, lowercase preference for UI labels.
| Element | Copy |
|---------|------|
| Primary CTA (new conversation) | "New conversation" (icon button with tooltip showing this text) |
| Send button tooltip | "Send message" |
| Chat panel toggle tooltip | "Open chat" / "Close chat" |
| Input placeholder | "Send a message..." |
| Empty state heading | "No conversations yet" |
| Empty state body | "Start a conversation to get help with your work." |
| Empty state action button | "New conversation" |
| Conversation list empty after filter | "No conversations match your search." |
| Error: failed to load conversations | "Couldn't load conversations. Check your connection and try again." |
| Error: failed to send message | "Message not sent. Try again." |
| Error: failed to create conversation | "Couldn't create conversation. Try again." |
| Delete confirmation (inline) | "Delete this conversation?" with "Delete" (destructive) and "Cancel" buttons |
| Conversation auto-title prefix | First 60 characters of the user's first message, no prefix label |
| Archive action label | "Archive" |
| Unarchive action label | "Unarchive" |
| Pin action label | "Pin" |
| Unpin action label | "Unpin" |
| Rename action label | "Rename" |
Destructive action in this phase:
- **Delete conversation**: triggered from DropdownMenu, confirmed inline (no Dialog). Confirmation text: "Delete this conversation?" Button labels: "Delete" (destructive variant) and "Cancel" (ghost variant).
---
## Accessibility Contract
- All icon-only buttons must have `aria-label`
- The chat panel must have `role="complementary"` and `aria-label="Chat"`
- The conversation list must be a `<nav aria-label="Conversations">`
- Active conversation item: `aria-current="true"`
- Loading skeleton items must have `aria-busy="true"` on the list container
- The message list must have `role="log"` and `aria-live="polite"` so screen readers announce new messages
- The chat input must have `aria-label="Message input"`
- The send button must have `aria-label="Send message"` and `aria-disabled` when empty
- Focus management: when a new conversation is created, move focus to the chat input
- Keyboard: full keyboard navigation within the conversation list via arrow keys (standard `rovingTabIndex` or `aria-activedescendant` pattern is acceptable; `tabIndex` cycling is sufficient for this phase)
---
## Animation Contract
All animations reuse existing CSS transition patterns from the codebase. No new animation libraries.
| Element | Animation | Duration | Easing |
|---------|-----------|----------|--------|
| Chat panel open/close | `transition-[width]` | 100ms | `ease-out` |
| Copy button → check icon | opacity + transform swap | 150ms | `ease-in-out` |
| Conversation list item hover | `transition-colors` | 150ms | `ease-out` (Tailwind default) |
| Send loading spinner | `animate-spin` (Tailwind) | continuous | linear |
| New message entry | no animation (Phase 21 scope) | — | — |
---
## Registry Safety
| Registry | Blocks Used | Safety Gate |
|----------|-------------|-------------|
| shadcn official | All existing components (Avatar, Badge, Button, Card, Dialog, DropdownMenu, Input, ScrollArea, Separator, Sheet, Skeleton, Tabs, Textarea, Tooltip) | not required |
| Third-party registries | none | not applicable |
No third-party registry blocks are declared for this phase. Only `rehype-highlight` (npm, not shadcn registry) is a new dependency — npm packages are not subject to the shadcn registry safety gate.
---
## 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