18 KiB
| phase | slug | status | shadcn_initialized | preset | created |
|---|---|---|---|---|---|
| 21 | chat-foundation | draft | true | new-york / neutral / css-variables | 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 withrehype-highlightfor syntax highlighting (CHAT-02/03)PanelContextpattern — mirror forChatPanelContextwithnexus:chat-panel-openkeyPropertiesPanel— reference layout; chat drawer must be a sibling, not a replacementScrollArea— 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 <PropertiesPanel> within the existing flex row in Layout.tsx.
[ CompanyRail ] [ Sidebar ] [ <main> ] [ ChatPanel ] [ PropertiesPanel ]
Rules:
- Width when open: 380px (matching
RESEARCH.mdrecommendation; same order-of-magnitude asPropertiesPanelat 320px) - Width when closed: 0px (
overflow-hidden) - Transition:
transition-[width] duration-100 ease-out— matches sidebar and PropertiesPanel animations exactly - When
ChatPanelopens,PropertiesPanelcloses (setpanelVisible: falseviasetPanelVisible). They do not coexist — the combined 700px would crowd a default 1280px viewport. - Desktop only initially:
hidden md:flex— same guard asPropertiesPanel localStoragekey 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-2for compact density - Code block padding:
padding: 0.5rem 0.65rem— inherited from existing.paperclip-markdown prerule inindex.css; not introduced or modified in Phase 21 - Touch targets: minimum 44px height on
@media (pointer: coarse)— already enforced globally inindex.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:
- Hovered conversation list row (
hover:bg-accent) - Currently active/selected conversation row (
bg-accent/60) - Code block toolbar background on hover (existing
index.csspattern) - 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 LayoutPlus— new conversation buttonPin/PinOff— pin/unpin conversation (dropdown action)Archive— archive conversation (dropdown action)Trash2— delete conversation (dropdown action, triggers confirmation)Copy— copy code block contentCheck— copy success state (shown 1500ms, then reverts to Copy)Send— send message buttonX— 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
ChatPanelbackgrounds usevar(--background)andvar(--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 ChatMarkdownMessagepassesTHEME_META[theme].darktorehype-highlighttheme selection- Code block syntax highlight CSS: load via scoped CSS selector approach (
.dark .hljs/.theme-tokyo-night .hljs), NOT via multiple<link>imports
Accessibility
| Concern | Requirement |
|---|---|
| Chat panel | <aside> with aria-label="Chat" |
| Conversation list | <ul> with role="listbox" or <nav>; active item has aria-current="true" |
| Message thread | <ol> with messages as <li>; aria-live="polite" on the list for new message announcements |
| Input submit | <form> wrapper with onSubmit; Enter key handled via form submission |
| Icon-only buttons | All have aria-label — see Copywriting Contract |
| Focus management | When chat panel opens, focus moves to the input textarea |
| Keyboard trap | Chat panel is NOT a modal — no focus trap; users navigate freely |
| Color contrast | All text uses existing CSS variables which are WCAG-AA compliant for their respective themes |
Registry Safety
| Registry | Blocks Used | Safety Gate |
|---|---|---|
| shadcn official | scroll-area, skeleton, button, textarea, dialog, dropdown-menu, tooltip (all already installed) |
not required |
| Third-party | none | not applicable |
No third-party registries used in Phase 21. All shadcn components are already installed in ui/src/components/ui/. The one new package (rehype-highlight) is an npm package installed via pnpm, not a shadcn registry block.
Animation and Motion
| Element | Animation | Duration | Easing |
|---|---|---|---|
| Chat panel open/close | transition-[width] |
100ms | ease-out — matches sidebar |
| New message insert | No animation (Phase 21; streaming animations in Phase 22) | — | — |
| Copy button icon swap | CSS transition opacity |
150ms | ease |
| Conversation row highlight (new) | activity-row-enter keyframe (reuse existing) |
520ms | cubic-bezier(0.16, 1, 0.3, 1) |
| Skeleton loading | Tailwind animate-pulse (shadcn default) |
continuous | — |
Reduced motion: Wrap conversation-row-enter in @media (prefers-reduced-motion: reduce) — matching the existing activity-row-enter guard in index.css.
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