diff --git a/.planning/phases/21-chat-foundation/21-UI-SPEC.md b/.planning/phases/21-chat-foundation/21-UI-SPEC.md new file mode 100644 index 00000000..47049dcb --- /dev/null +++ b/.planning/phases/21-chat-foundation/21-UI-SPEC.md @@ -0,0 +1,342 @@ +--- +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). + +### 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` — matches existing `.paperclip-markdown pre` rule in `index.css` +- 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 | 14px (0.875rem) | 500 | 1.4 | Code block language labels, drawer section headers | +| 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. + +**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 | "Cancel" | `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 | `