nexus/.planning/phases/21-chat-foundation/21-UI-SPEC.md

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 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 <PropertiesPanel> within the existing flex row in Layout.tsx.

[ CompanyRail ] [ Sidebar ] [ <main> ] [ 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 <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