nexus/.planning/phases/21-chat-foundation/21-UI-SPEC.md
Mikkel Georgsen 08d01f89c9 [nexus] docs(21): fix UI-SPEC checker issues — copywriting, typography, spacing
- 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>
2026-04-01 12:40:17 +02:00

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 existing EntityRow pattern 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 use sm+ for list item vertical padding.
  • Touch targets on coarse-pointer devices: min-height: 44px (already enforced globally in index.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-medium or font-semibold to conversation titles — regular weight at 13px provides sufficient legibility without visual noise.
  • Agent message content renders inside MarkdownBody which applies prose prose-sm — do not override prose typography for markdown content.
  • The chat input Textarea uses 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-semibold to 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:

  1. The "Send message" / "New conversation" primary action button background
  2. The active conversation in the sidebar (left-border indicator: border-l-2 border-primary)
  3. 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 theme base16/catppuccin (dark variant via .dark class)
  • tokyo-night (dark): use highlight.js theme tokyo-night-dark (via .theme-tokyo-night.dark class)
  • catppuccin-latte (light): use highlight.js theme base16/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 in Layout.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 (use nexus: prefix, not paperclip:)

ChatConversationList (inside ChatPanel left column, width: 240px)

  • Background: bg-sidebar (secondary surface)
  • Item height: 48px (py-3 px-3) — uses sm+ (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-primary when active
  • Archive icon: 14px, text-muted-foreground
  • Skeleton loader: 3 skeleton items (<Skeleton>) while isLoading
  • Infinite scroll sentinel: <div ref={sentinelRef}> at bottom of list

ChatMessageList (main chat area)

  • Background: bg-background
  • Padding: p-4 with gap-4 between messages
  • User message: right-aligned, bg-secondary text-secondary-foreground, rounded-none (matches --radius: 0 global setting), max-w-[75%], padding px-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 MarkdownBody with rehype-highlight added to rehypePlugins
  • Code block container: bg-card border border-border, relative positioning 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) with field-sizing: content CSS + scroll-height fallback
  • Min height: 40px (one line)
  • Max height: 160px (approx 6 lines), then scrolls internally
  • Padding: px-3 py-2 (8px vertical — sm token)
  • Background: bg-muted (matches --muted for 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) → submit
    • Shift+Enter → newline
    • Escape → 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: IntersectionObserver on 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 disabled and shows a Loader2 spin 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" and aria-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" and aria-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" and aria-disabled when 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 rovingTabIndex or aria-activedescendant pattern is acceptable; tabIndex cycling 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