14 KiB
| phase | slug | status | shadcn_initialized | preset | created |
|---|---|---|---|---|---|
| 21 | chat-foundation | draft | true | new-york / neutral / cssVariables | 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 inindex.css@media (pointer: coarse)) - Sidebar conversation list item: 12px vertical padding (
py-3) — between sm and md — acceptable as it follows existingEntityRowpattern - 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
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).
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" / "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) - 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-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.5 - 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 / 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:
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 (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"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