[nexus] docs(21): UI design contract
This commit is contained in:
parent
72764b1e93
commit
181af7b00d
1 changed files with 273 additions and 0 deletions
273
.planning/phases/21-chat-foundation/21-UI-SPEC.md
Normal file
273
.planning/phases/21-chat-foundation/21-UI-SPEC.md
Normal file
|
|
@ -0,0 +1,273 @@
|
|||
---
|
||||
phase: 21
|
||||
slug: chat-foundation
|
||||
status: draft
|
||||
shadcn_initialized: true
|
||||
preset: new-york / neutral / cssVariables
|
||||
created: 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.
|
||||
|
||||
---
|
||||
|
||||
## 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 |
|
||||
| md | 16px | Default element padding (`p-4`), conversation list item padding |
|
||||
| lg | 24px | Section padding on desktop (`p-6`), chat panel header padding |
|
||||
| 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:
|
||||
- Touch targets on coarse-pointer devices: `min-height: 44px` (already enforced globally in `index.css` `@media (pointer: coarse)`)
|
||||
- Sidebar conversation list item: 12px vertical padding (`py-3`) — between sm and md — acceptable as it follows existing `EntityRow` pattern
|
||||
- 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`.
|
||||
|
||||
| Role | Size | Weight | Line Height | Tailwind Class |
|
||||
|------|------|--------|-------------|----------------|
|
||||
| Body | 14px | 400 (regular) | 1.5 | `text-sm` |
|
||||
| Label | 13px | 500 (medium) | 1.4 | `text-[13px] font-medium` |
|
||||
| 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 / medium).
|
||||
- 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).
|
||||
|
||||
---
|
||||
|
||||
## 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" / "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`)
|
||||
- Active item: `border-l-2 border-primary bg-sidebar-accent`
|
||||
- Hover item: `bg-sidebar-accent/50`
|
||||
- Title: Label (13px / medium), 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.5`
|
||||
- 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 / Unpin | Pin / PinOff | default foreground | None — immediate |
|
||||
| Archive | Archive | default foreground | None — immediate |
|
||||
| Delete | Trash2 | `text-destructive` | Inline confirm: replace DropdownMenu trigger with "Delete?" + "Yes" / "No" buttons inside the same popover |
|
||||
|
||||
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" 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 (inline) | "Delete this conversation?" with "Delete" (destructive) and "Cancel" buttons |
|
||||
| Conversation auto-title prefix | First 60 characters of the user's first message, no prefix label |
|
||||
| Archive action label | "Archive" |
|
||||
| Unarchive action label | "Unarchive" |
|
||||
| Pin action label | "Pin" |
|
||||
| Unpin action label | "Unpin" |
|
||||
| Rename action label | "Rename" |
|
||||
|
||||
Destructive action in this phase:
|
||||
- **Delete conversation**: triggered from DropdownMenu, confirmed inline (no Dialog). Confirmation text: "Delete this conversation?" Button labels: "Delete" (destructive variant) and "Cancel" (ghost variant).
|
||||
|
||||
---
|
||||
|
||||
## 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
|
||||
Loading…
Add table
Reference in a new issue