--- phase: 21 slug: chat-foundation status: draft shadcn_initialized: true preset: new-york / neutral / css-variables created: 2026-04-01 --- # Phase 21 — UI Design Contract > Visual and interaction contract for Phase 21: Chat Foundation. > Generated by gsd-ui-researcher. Verified by gsd-ui-checker. --- ## Design System | Property | Value | Source | |----------|-------|--------| | Tool | shadcn/ui | `ui/components.json` | | Style | new-york | `ui/components.json` | | Base color | neutral | `ui/components.json` | | CSS variables | true | `ui/components.json` | | Component library | Radix UI (via shadcn new-york) | `ui/components.json` | | Icon library | lucide-react ^0.574.0 | `ui/components.json` + RESEARCH.md | | Font | System UI (`font-sans` from Tailwind default, inherited) | `ui/src/index.css` | **Existing shadcn components available (no install needed):** `avatar`, `badge`, `button`, `card`, `checkbox`, `collapsible`, `command`, `dialog`, `dropdown-menu`, `input`, `label`, `popover`, `scroll-area`, `select`, `separator`, `sheet`, `skeleton`, `tabs`, `textarea`, `tooltip` **Existing custom components to reuse/extend:** - `MarkdownBody` — extend with `rehype-highlight` for syntax highlighting (CHAT-02/03) - `PanelContext` pattern — mirror for `ChatPanelContext` with `nexus:chat-panel-open` key - `PropertiesPanel` — reference layout; chat drawer must be a sibling, not a replacement - `ScrollArea` — use for conversation list and message thread scroll regions --- ## Layout Contract ### Chat Drawer Position The `ChatPanel` is a fixed-width right-side drawer, positioned as a sibling of `` within the existing `flex` row in `Layout.tsx`. ``` [ CompanyRail ] [ Sidebar ] [
] [ ChatPanel ] [ PropertiesPanel ] ``` **Rules:** - Width when open: 380px (matching `RESEARCH.md` recommendation; same order-of-magnitude as `PropertiesPanel` at 320px) - Width when closed: 0px (`overflow-hidden`) - Transition: `transition-[width] duration-100 ease-out` — matches sidebar and PropertiesPanel animations exactly - When `ChatPanel` opens, `PropertiesPanel` closes (set `panelVisible: false` via `setPanelVisible`). They do not coexist — the combined 700px would crowd a default 1280px viewport. - Desktop only initially: `hidden md:flex` — same guard as `PropertiesPanel` - `localStorage` key for open state: `nexus:chat-panel-open` ### Chat Drawer Internal Layout ``` [ Header: title + close button ] — border-b border-border, px-4 py-2 [ ChatConversationList ] — fixed width left column, 240px, border-r [ ChatMessageList ] — flex-1, overflow-auto via ScrollArea [ ChatInput ] — sticky bottom, border-t border-border ``` Alternatively, the drawer can be a single-column panel toggling between conversation list view and message thread view (simpler for Phase 21; agent streaming in Phase 22 makes the two-column layout more valuable). **Use two-column layout within the 380px drawer**: left column 160px (conversation list), right column flex-1 (message thread + input). **Focal point:** Primary visual anchor: `ChatMessageList` (flex-1 thread pane); secondary anchor: `ChatInput` (sticky bottom). ### Chat Panel Trigger Add a `MessageSquare` icon button to the top-right control area of `Layout.tsx` (or to `BreadcrumbBar`), using the same `Button variant="ghost" size="icon-sm"` pattern as the existing theme toggle and settings buttons. --- ## Spacing Scale Declared values (all multiples of 4): | Token | Value | Usage | |-------|-------|-------| | xs | 4px | Icon gaps (`gap-1`), inline chip padding | | sm | 8px | Compact element spacing, badge padding (`px-2 py-1`) | | md | 16px | Default element spacing, panel padding (`p-4`) | | lg | 24px | Section padding (`px-6`), message bubble vertical rhythm | | xl | 32px | Layout gaps between major zones | | 2xl | 48px | Not used in Phase 21 (no full-page sections) | | 3xl | 64px | Not used in Phase 21 | **Exceptions:** - Chat input area: `px-3 py-2` (12px/8px) — matches existing BreadcrumbBar footer pattern - Message bubble padding: `px-3 py-2` for compact density - Code block padding: `padding: 0.5rem 0.65rem` — inherited from existing `.paperclip-markdown pre` rule in `index.css`; not introduced or modified in Phase 21 - Touch targets: minimum 44px height on `@media (pointer: coarse)` — already enforced globally in `index.css` --- ## Typography All sizes and weights are drawn from the existing `.paperclip-markdown` and `index.css` typographic system. No new type tokens are introduced. | Role | Size | Weight | Line Height | Usage | |------|------|--------|-------------|-------| | Body / message text | 15px (0.9375rem) | 400 | 1.6 | Chat message prose (`paperclip-markdown` font-size: 0.9375rem; line-height: 1.6) | | Label / UI chrome | 13px (0.8125rem) | 400 | 1.4 | Conversation list titles, timestamps, sidebar nav (`text-[13px]` already used in Layout) | | Subheading | 13px (0.8125rem) | 400 | 1.4 | Code block language labels, drawer section headers — differentiated from body by context and container, not size alone | | Heading (in markdown) | 20px (1.25rem) | 600 | 1.3 | `## ` headings inside agent responses (`.paperclip-markdown h2`) | **Weights used:** 400 (regular) and 600 (semibold). No additional weights. **Note on Subheading vs Label:** Both are 13px / 400. Subheading elements (language labels, section headers) are differentiated from body prose (15px) by a 2px size difference and by their placement within distinct UI containers (code block toolbar, panel header), not by a separate weight. A 14px intermediate size was considered and rejected — the 1px gap from 15px body was insufficient differentiation without a weight cue. **Monospace font (code blocks):** `ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace` — matches existing `.paperclip-markdown code` declaration. --- ## Color The design uses the existing Nexus CSS variable system across all three themes. No new color values are introduced. | Role | Catppuccin Mocha (.dark) | Tokyo Night (.theme-tokyo-night.dark) | Catppuccin Latte (:root) | Usage | |------|--------------------------|---------------------------------------|--------------------------|-------| | Dominant (60%) | `--background` #1e1e2e | `--background` #1a1b26 | `--background` #eff1f5 | Chat panel background, message list | | Secondary (30%) | `--card` #181825 / `--sidebar` #181825 | `--card` #16161e / `--sidebar` #16161e | `--card` #e6e9ef | Conversation list pane, code block background, input area | | Accent (10%) | `--accent` #45475a | `--accent` #3b4261 | `--accent` #bcc0cc | Hovered conversation row, active conversation highlight | | Primary | `--primary` #89b4fa | `--primary` #7aa2f7 | `--primary` #1e66f5 | Send button, focus rings, active conversation indicator | | Destructive | `--destructive` #f38ba8 | `--destructive` #f7768e | `--destructive` #d20f39 | Delete conversation confirmation only | | Muted text | `--muted-foreground` #6c7086 | `--muted-foreground` #565f89 | `--muted-foreground` #9ca0b0 | Timestamps, conversation preview text, empty state body | **Accent reserved for:** 1. Hovered conversation list row (`hover:bg-accent`) 2. Currently active/selected conversation row (`bg-accent/60`) 3. Code block toolbar background on hover (existing `index.css` pattern) 4. Input area focus-within ring (`focus-within:ring-1 ring-ring`) **Accent is NOT used for:** send button, message bubbles, badges, or any primary action affordance. ### Code Block Syntax Highlighting — Theme Mapping | Nexus Theme | highlight.js CSS Theme | `THEME_META[theme].dark` | |-------------|----------------------|--------------------------| | catppuccin-mocha | `highlight.js/styles/base16/catppuccin.css` (dark variant) | true | | tokyo-night | `highlight.js/styles/tokyo-night-dark.css` | true | | catppuccin-latte | `highlight.js/styles/base16/catppuccin.css` (light variant) | false | Theme CSS must be loaded via a single CSS file that gates `.hljs` color overrides on `.dark` and `.theme-tokyo-night` — never import all three theme CSS files simultaneously. Override approach: use CSS custom properties per theme class, matching the existing pattern in `index.css`. **Existing code block colors (hardcoded in index.css, keep consistent):** - Dark code block background: `#1e1e2e` (Catppuccin Mocha base) - Dark code block text: `#cdd6f4` - These hardcoded values apply for Mocha; hljs theme overrides layer on top for Tokyo Night --- ## Component Inventory Components to build in Phase 21: | Component | shadcn base | Notes | |-----------|-------------|-------| | `ChatPanelContext.tsx` | none | localStorage persistence, mirrors `PanelContext` pattern | | `ChatPanel.tsx` | `ScrollArea`, `Button` | Right drawer shell; two-column (list + thread) | | `ChatConversationList.tsx` | `ScrollArea`, `skeleton`, `button` | Infinite scroll via `useInfiniteQuery`; skeleton on load | | `ChatConversationItem.tsx` | `dropdown-menu`, `button` | Row with title, preview, timestamp; hover reveals action menu | | `ChatMessageList.tsx` | `ScrollArea` | Message thread; virtualization deferred to Phase 22 | | `ChatMessage.tsx` | none | Wrapper for user vs assistant messages; role-based alignment | | `ChatMarkdownMessage.tsx` | none | Extends `MarkdownBody` with `rehype-highlight`; adds copy button on `pre` | | `ChatInput.tsx` | `textarea`, `button` | Auto-resize textarea; keyboard shortcuts | | `ChatCodeBlock.tsx` | `button`, `tooltip` | Wraps `pre`; adds language label + copy button | **Icons (lucide-react):** - `MessageSquare` — chat panel trigger button in Layout - `Plus` — new conversation button - `Pin` / `PinOff` — pin/unpin conversation (dropdown action) - `Archive` — archive conversation (dropdown action) - `Trash2` — delete conversation (dropdown action, triggers confirmation) - `Copy` — copy code block content - `Check` — copy success state (shown 1500ms, then reverts to Copy) - `Send` — send message button - `X` — close chat panel, dismiss input --- ## Interaction Contract ### Conversation List | Interaction | Behavior | |-------------|---------| | Click conversation row | Load that conversation's messages into the thread pane | | Hover conversation row | Reveal `...` (MoreHorizontal) icon button at row end | | Click `...` | Open `dropdown-menu` with: Rename, Pin/Unpin, Archive, Delete | | Click `+` New conversation | Create new conversation via POST, optimistic insert at top of list | | Scroll to bottom of list | Trigger `fetchNextPage` for infinite scroll (intersection observer on last item) | | Long-press (mobile) | Same as hover — reveal action menu | ### Conversation CRUD States | Action | UI Response | |--------|------------| | Create conversation | Optimistic insert at top of list; title shows "New Conversation" placeholder until first message auto-sets it | | Rename (title edit) | Inline contenteditable or input field replacing the title text; blur or Enter confirms; Escape cancels | | Pin conversation | Row moves to top of list within a "Pinned" group (visually separated); pin icon visible on row | | Archive conversation | Row removed from default list; no undo in Phase 21 | | Delete conversation | Confirmation dialog (shadcn `dialog`): "Delete conversation? This cannot be undone." with "Delete" (destructive) + "Cancel" buttons | ### Message Thread | Interaction | Behavior | |-------------|---------| | User message | Right-aligned bubble, `bg-secondary` background, `text-secondary-foreground` | | Assistant message | Left-aligned, no bubble background, full width, `ChatMarkdownMessage` renders content | | New message sent | Textarea clears; optimistic message appends at bottom; thread scrolls to bottom | | Thread empty state | Centered message: "No messages yet" + instruction text (see Copywriting) | ### Chat Input | Interaction | Behavior | |-------------|---------| | Type in textarea | Auto-resizes: `field-sizing: content` (fallback: scrollHeight calculation); max-height 160px before scrolling | | Enter (no modifier) | Submit message via `chatApi.postMessage`; clears textarea | | Shift+Enter | Insert newline; do not submit | | Escape | Clear textarea content if non-empty; if already empty, do nothing | | Send button click | Same as Enter; button is disabled when textarea is empty | ### Code Block Copy Button | Interaction | Behavior | |-------------|---------| | Hover code block | Copy button (`Copy` icon) appears in top-right corner of `pre` block | | Click copy | `navigator.clipboard.writeText(code)` executes; icon changes to `Check` for 1500ms; then reverts to `Copy` | | Copy failure | Log to console; no user-visible error (local trusted mode) | ### Keyboard Shortcuts (INPUT-07) | Shortcut | Scope | Action | |----------|-------|--------| | Enter | Chat input focused | Send message | | Shift+Enter | Chat input focused | Insert newline | | Escape | Chat input focused | Clear input | | Cmd+K | Global | Opens search (Phase 24 — wire shortcut now, handler is no-op in Phase 21) | --- ## Copywriting Contract | Element | Copy | Notes | |---------|------|-------| | Chat panel toggle button aria-label | "Open chat" / "Close chat" | Toggle based on `chatOpen` state | | New conversation button | "New conversation" | Tooltip + aria-label | | Conversation list empty state heading | "No conversations yet" | Shown when list is empty | | Conversation list empty state body | "Start a conversation to get help from your agents." | Directs user toward action | | Message thread empty state | "Send a message to start this conversation." | Shown when conversation exists but has no messages | | Delete conversation dialog title | "Delete conversation?" | shadcn Dialog title | | Delete conversation dialog body | "This conversation and all its messages will be permanently deleted." | Confirms scope | | Delete confirmation button | "Delete" | `variant="destructive"` | | Delete cancel button | "Keep conversation" | `variant="outline"` | | Archive action label | "Archive" | Dropdown menu item | | Pin action label | "Pin" / "Unpin" | Toggle based on pinned state | | Rename action label | "Rename" | Dropdown menu item | | Input placeholder | "Message your agent..." | Textarea placeholder | | Send button aria-label | "Send message" | Icon-only button needs label | | Copy code button aria-label | "Copy code" | Before copy; reverts after success | | Copy code success aria-label | "Copied!" | 1500ms, then reverts | | Title auto-generated pattern | First 60 chars of first message, truncated with ellipsis | Server-side; shown in list immediately | **Tone:** Direct, functional, no corporate language. No exclamation marks except "Copied!" (which is a status, not marketing). --- ## States and Loading | Component | Loading state | Empty state | Error state | |-----------|--------------|-------------|-------------| | `ChatConversationList` | 5x `Skeleton` rows (h-10, w-full, rounded) | "No conversations yet" with CTA | "Could not load conversations. Refresh to try again." | | `ChatMessageList` | No skeleton — load is fast (persisted local server) | "Send a message to start this conversation." | "Could not load messages. Refresh to try again." | | `ChatInput` send | Send button shows `Loader2` spinning icon while POST is in flight; disabled state | n/a | Toast: "Message failed to send. Try again." | **Optimistic updates:** New conversations and new messages insert immediately into the UI before server confirmation. On failure, remove the optimistic item and show the error toast. --- ## Theme Integration Contract Requirements THEME-01 and THEME-02 require zero new plumbing — `useTheme()` + `THEME_META` + existing CSS variables handle everything. **Checklist:** - All `ChatPanel` backgrounds use `var(--background)` and `var(--card)` — never hardcoded hex - Conversation list hover uses `hover:bg-accent` — resolves correctly in all three themes - Border colors use `border-border` — resolves correctly in all three themes - `ChatMarkdownMessage` passes `THEME_META[theme].dark` to `rehype-highlight` theme selection - Code block syntax highlight CSS: load via scoped CSS selector approach (`.dark .hljs` / `.theme-tokyo-night .hljs`), NOT via multiple `` imports --- ## Accessibility | Concern | Requirement | |---------|-------------| | Chat panel | `