- Replace "Cancel" with "Keep conversation" in delete confirmation (Dimension 1)
- Drop font-weight 500 (medium); consolidate to 400 + 600 only (Dimension 4)
- Fix ChatInput padding from py-2.5 (10px) to py-2 (8px) (Dimension 5)
- Name 12px as sm+ token with justification for conversation list item padding (Dimension 5)
- Add noun suffix to all dropdown action labels ("Archive conversation", etc.)
- Add focal point statement to contract
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
15 KiB
| phase | slug | status | shadcn_initialized | preset | created | revised |
|---|---|---|---|---|---|---|
| 21 | chat-foundation | draft | true | new-york / neutral / cssVariables | 2026-04-01 | 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.
Focal Point
The primary focal point of this phase is the chat input at the bottom of the ChatPanel. When the panel opens, focus moves immediately to the ChatInput textarea. All surrounding elements (conversation list, message thread, header) are secondary supporting surfaces.
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 |
| sm+ | 12px | Conversation list item vertical padding (py-3) — named exception; follows existing EntityRow pattern; sits between sm and md |
| md | 16px | Default element padding (p-4), chat panel header padding |
| lg | 24px | Section padding on desktop (p-6) |
| 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:
sm+(12px) is a named token for conversation list item vertical padding. Justification: follows the existingEntityRowpattern throughout the codebase. It is not a one-off magic number — it is a deliberate in-between value that keeps list items readable without wasting vertical space. Only usesm+for list item vertical padding.- Touch targets on coarse-pointer devices:
min-height: 44px(already enforced globally inindex.css@media (pointer: coarse)) - 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.
Two weights only: regular (400) and semibold (600). Medium (500) is not used in this phase.
| Role | Size | Weight | Line Height | Tailwind Class |
|---|---|---|---|---|
| Body | 14px | 400 (regular) | 1.5 | text-sm |
| Label | 13px | 400 (regular) | 1.4 | text-[13px] |
| 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 / regular). Do not apply
font-mediumorfont-semiboldto conversation titles — regular weight at 13px provides sufficient legibility without visual noise. - Agent message content renders inside
MarkdownBodywhich appliesprose prose-sm— do not override prose typography for markdown content. - The chat input
Textareauses 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).
- Active conversation title in the sidebar: do NOT use
font-semiboldto indicate active state. Use the left-border accent indicator instead (see Color contract).
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:
- The "Send message" / "New conversation" primary action button background
- The active conversation in the sidebar (left-border indicator:
border-l-2 border-primary) - 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 themebase16/catppuccin(dark variant via.darkclass)tokyo-night(dark): use highlight.js themetokyo-night-dark(via.theme-tokyo-night.darkclass)catppuccin-latte(light): use highlight.js themebase16/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 inLayout.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(usenexus:prefix, notpaperclip:)
ChatConversationList (inside ChatPanel left column, width: 240px)
- Background:
bg-sidebar(secondary surface) - Item height: 48px (
py-3 px-3) — usessm+(12px) vertical padding token - Active item:
border-l-2 border-primary bg-sidebar-accent - Hover item:
bg-sidebar-accent/50 - Title: Label (13px / regular), truncated with
truncate - Timestamp: Meta (12px / muted-foreground), right-aligned
- Pin icon: 14px,
text-primarywhen active - Archive icon: 14px,
text-muted-foreground - Skeleton loader: 3 skeleton items (
<Skeleton>) whileisLoading - Infinite scroll sentinel:
<div ref={sentinelRef}>at bottom of list
ChatMessageList (main chat area)
- Background:
bg-background - Padding:
p-4withgap-4between messages - User message: right-aligned,
bg-secondary text-secondary-foreground,rounded-none(matches--radius: 0global setting),max-w-[75%], paddingpx-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
MarkdownBodywithrehype-highlightadded torehypePlugins - Code block container:
bg-card border border-border,relativepositioning 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) withfield-sizing: contentCSS + scroll-height fallback - Min height: 40px (one line)
- Max height: 160px (approx 6 lines), then scrolls internally
- Padding:
px-3 py-2(8px vertical —smtoken) - Background:
bg-muted(matches--mutedfor 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) → submitShift+Enter→ newlineEscape→ 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 conversation / Unpin conversation | Pin / PinOff | default foreground | None — immediate |
| Archive conversation | Archive | default foreground | None — immediate |
| Unarchive conversation | ArchiveRestore | default foreground | None — immediate |
| Delete conversation | Trash2 | text-destructive |
Inline confirm: replace DropdownMenu trigger with popover containing "Delete conversation?" + "Delete conversation" (destructive) / "Keep conversation" (ghost) buttons |
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 conversation" 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:
IntersectionObserveron 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
disabledand shows aLoader2spin 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 question (inline) | "Delete this conversation?" |
| Delete confirmation — destructive button | "Delete conversation" |
| Delete confirmation — dismiss button | "Keep conversation" |
| Conversation auto-title prefix | First 60 characters of the user's first message, no prefix label |
| Archive action label | "Archive conversation" |
| Unarchive action label | "Unarchive conversation" |
| Pin action label | "Pin conversation" |
| Unpin action label | "Unpin conversation" |
| Rename action label | "Rename conversation" |
Destructive action in this phase:
- Delete conversation: triggered from DropdownMenu, confirmed inline (no Dialog). Confirmation text: "Delete this conversation?" Button labels: "Delete conversation" (destructive variant) and "Keep conversation" (ghost variant). "Keep conversation" is the explicit dismissal — it communicates what is preserved, not just that the action was cancelled.
Accessibility Contract
- All icon-only buttons must have
aria-label - The chat panel must have
role="complementary"andaria-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"andaria-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"andaria-disabledwhen 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
rovingTabIndexoraria-activedescendantpattern is acceptable;tabIndexcycling 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