# Story 2.2: Draft View UI (The Slide-Up) Status: done ## Story As a user, I want to view the generated draft in a clean, reading-focused interface, So that I can review it without the distraction of the chat. ## Acceptance Criteria 1. **Draft View Slide-Up Display** - Given the draft generation is complete - When the result is ready - Then a "Sheet" or modal slides up from the bottom - And it displays the post in "Medium-style" typography (Merriweather font) 2. **Comfortable Reading Experience** - Given the draft view is open - When the user scrolls - Then the reading experience is comfortable with appropriate whitespace - And the "Thumbs Up" and "Thumbs Down" actions are sticky or easily accessible ## Tasks / Subtasks - [x] Implement Draft View Sheet Component - [x] Create `Sheet.tsx` slide-up component (custom implementation, no ShadCN) - [x] Configure slide-up animation from bottom (300ms ease-out) - [x] Set up responsive behavior (full-screen on mobile, centered card on desktop) - [x] Implement backdrop/dim overlay with tap-to-close - [x] Implement Draft Content Display - [x] Create `DraftContent.tsx` component for Markdown rendering - [x] Apply Merriweather serif font for content (UI font stays Inter) - [x] Add generous whitespace and line-height for readability - [x] Style headings, paragraphs, and code blocks following Medium-style - [x] Add tag display section - [x] Implement Action Bar (Thumbs Up/Down) - [x] Create `DraftActions.tsx` component with sticky footer - [x] Add Thumbs Up button (approve/copy) - [x] Add Thumbs Down button (regenerate feedback) - [x] Style buttons to be touch-friendly (44px min height) - [x] Add proper ARIA labels for accessibility - [x] Integrate with ChatStore - [x] Connect `currentDraft` state to DraftViewSheet - [x] Auto-open sheet when `currentDraft` transitions from null to populated - [x] Handle sheet close action (clear currentDraft and showDraftView) - [x] Use atomic selectors for state access - [x] Implement Copy to Clipboard (Thumbs Up) - [x] Implement `copyToClipboard()` utility inline in ChatStore - [x] Copy to clipboard with fallback for older browsers - [x] Mark draft as 'completed' in IndexedDB - [x] Implement Regeneration Flow (Thumbs Down) - [x] Close DraftViewSheet on Thumbs Down tap - [x] System message to chat: "What should we change?" (Story 2.3 will add this) - [x] Set chat input focus for user feedback - [x] Preserve draft context for regeneration in Story 2.3 - [x] Implement Responsive Layout - [x] Mobile (< 768px): Full-screen sheet - [x] Desktop (>= 768px): Centered card layout (max-width ~600px) - [x] Ensure sheet doesn't stretch too wide on large screens - [x] Test keyboard navigation (Escape to close) - [x] Test Draft View End-to-End - [x] Unit test: Sheet renders in closed state by default - [x] Unit test: Sheet opens when open prop is true - [x] Unit test: Markdown rendering handles code blocks, lists, links - [x] Unit test: DraftContent handles empty tags array - [x] Integration test: Sheet auto-opens after Ghostwriter completes - [x] Integration test: Thumbs Up copies to clipboard and marks completed - [x] Integration test: Thumbs Down returns to chat with feedback prompt - [x] Integration test: DraftViewSheet full flow with all components ## Dev Notes ### Architecture Compliance (CRITICAL) **Logic Sandwich Pattern - DO NOT VIOLATE:** - **UI Components** MUST NOT import `src/lib/db` or touch IndexedDB directly - Draft status updates MUST go through `ChatService` -> `DraftService` - Components use Zustand store via atomic selectors only - Services return plain objects, not Dexie observables **State Management - Atomic Selectors Required:** ```typescript // BAD - Causes unnecessary re-renders const { currentDraft, showDraftView } = useChatStore(); // GOOD - Atomic selectors const currentDraft = useChatStore(s => s.currentDraft); const showDraftView = useChatStore(s => s.showDraftView); const closeDraftView = useChatStore(s => s.closeDraftView); const approveDraft = useChatStore(s => s.approveDraft); const rejectDraft = useChatStore(s => s.rejectDraft); ``` **Local-First Data Boundary:** - Draft content already stored in IndexedDB (from Story 2.1) - Thumbs Up action updates draft status to 'completed' - Draft content remains in IndexedDB for history access (Epic 3) - No draft content sent to server for any reason ### Architecture Implementation Details **Story Purpose:** This story implements the **"Magic Moment"** UI - the visual transformation from casual chat to polished artifact. The slide-up sheet creates a distinct mode shift that reinforces the value the Ghostwriter added. This is the climax of the user journey. **State Management Extensions:** ```typescript // Add to ChatStore (src/lib/store/chat-store.ts) interface ChatStore { // Draft view state showDraftView: boolean; closeDraftView: () => void; approveDraft: (draftId: string) => Promise; rejectDraft: (draftId: string, feedback?: string) => void; } ``` **Component Architecture:** ``` DraftViewSheet (ShadCN Sheet wrapper) ├── DraftHeader (Title + Close button) ├── DraftContent (Markdown rendering with Merriweather font) │ └── TagList (Tag chips) └── DraftActions (Sticky footer with Thumbs Up/Down) ``` **Logic Flow:** 1. Ghostwriter completes generation (Story 2.1) 2. ChatStore sets `currentDraft` with generated draft 3. `showDraftView` becomes `true` (auto-trigger) 4. DraftViewSheet slides up from bottom 5. User reviews content in comfortable reading view 6. User taps Thumbs Up OR Thumbs Down: - **Thumbs Up**: Copy to clipboard, mark as 'completed', show success animation - **Thumbs Down**: Close sheet, return to chat, prompt for feedback 7. Sheet closes and flow continues **Responsive Behavior:** - **Mobile (< 768px)**: Full-screen sheet (covers chat completely) - **Desktop (>= 768px)**: Centered card (max-width 600px) with visible backdrop - This "Centered App" pattern keeps the mobile-first feel on desktop **Files to Create:** - `src/components/features/draft/DraftViewSheet.tsx` - Main sheet component - `src/components/features/draft/DraftContent.tsx` - Markdown renderer with Merriweather - `src/components/features/draft/DraftActions.tsx` - Thumbs Up/Down footer - `src/lib/utils/clipboard.ts` - Copy to clipboard utility **Files to Modify:** - `src/lib/store/chat-store.ts` - Add draft view state and actions - `src/services/draft-service.ts` - Add `updateDraftStatus()` method - `src/services/chat-service.ts` - Add `approveDraft()` and `rejectDraft()` orchestration ### UX Design Specifications **From UX Design Document:** **The "Magic Moment" Visualization:** - Clear visual shift from "Chat" (casual) to "Draft" (professional) - Sheet slide-up animation is critical for emotional payoff - Chat should remain visible underneath (context preserved) **Typography - The "Split-Personality" UI:** - **Chat UI (Input)**: Inter font (sans-serif) - casual, fast - **Draft UI (Output)**: Merriweather font (serif) - published, authoritative - This font change signals the transformation value **Visual Design - "Morning Mist" Theme:** ```css /* Draft Content Styling */ .draft-content { font-family: 'Merriweather', serif; line-height: 1.8; color: #334155; /* Deep Slate */ padding: 24px; } .draft-title { font-size: 1.75rem; font-weight: 700; margin-bottom: 1.5rem; color: #1E293B; } .draft-body { font-size: 1.125rem; margin-bottom: 2rem; } /* Tags */ .tag-chip { background: #E2E8F0; color: #475569; padding: 4px 12px; border-radius: 9999px; font-size: 0.875rem; font-family: 'Inter', sans-serif; } ``` **Action Bar - Sticky Footer:** - Thumbs Up (Approve): Primary action, brand color (Slate Blue #64748B) - Thumbs Down (Reject): Secondary action, outline style - Minimum 44px touch targets (WCAG AA) - Position: sticky at bottom of sheet - ARIA labels: "Approve and copy to clipboard", "Request changes" **Responsive - "Centered App" Pattern:** - Desktop view should NOT stretch to full width - Centered card container with max-width ~600px - Generous whitespace background (Morning Mist #F8FAFC) **Animation - Slide-Up Transition:** - Duration: 300ms (ease-out) - Should feel like "unveiling" the result - No bounce or overshoot (feels professional, not playful) ### Previous Story Intelligence (from Story 2.1) **Patterns Established (must follow):** - **Logic Sandwich Pattern:** UI -> Zustand -> Service -> Database (strictly enforced) - **Atomic Selectors:** All state access uses `useChatStore(s => s.field)` - **Draft Storage:** Drafts already stored in IndexedDB via `drafts` table - **Ghostwriter Integration:** `currentDraft` state already in ChatStore - **DraftingIndicator:** Already shows during generation (Story 2.1) **Key Files from Story 2.1 (Reference):** - `src/lib/db/draft-service.ts` - Draft CRUD operations (add status update method) - `src/lib/db/index.ts` - Drafts table with status field ('draft' | 'completed' | 'regenerated') - `src/lib/store/chat-store.ts` - Has `currentDraft`, add draft view state - `src/components/features/chat/DraftingIndicator.tsx` - Pattern for loading states **Learnings to Apply:** - Story 2.1 established the Draft data structure (id, title, content, tags, status) - Draft is already persisted to IndexedDB when Ghostwriter completes - Use the same DraftRecord interface from Story 2.1 - Follow the same testing pattern: unit tests for components, integration tests for flow **Draft Data Structure (from Story 2.1):** ```typescript interface DraftRecord { id: string; sessionId: string; title: string; content: string; // Markdown formatted tags: string[]; createdAt: number; status: 'draft' | 'completed' | 'regenerated'; } ``` **Integration with Ghostwriter:** - When Ghostwriter completes, `currentDraft` is already set in ChatStore - This story should auto-open DraftViewSheet when `currentDraft` changes - Use `useEffect` to watch for `currentDraft` and set `showDraftView = true` ### Testing Requirements **Unit Tests:** - `DraftViewSheet`: Renders draft content correctly - `DraftViewSheet`: Renders in closed state by default - `DraftViewSheet`: Opens when showDraftView is true - `DraftContent`: Renders Markdown with proper styling - `DraftContent`: Handles code blocks, lists, links correctly - `DraftActions`: Thumbs Up button calls approveDraft callback - `DraftActions`: Thumbs Down button calls rejectDraft callback - `ClipboardUtils`: copyToClipboard() writes to clipboard correctly **Integration Tests:** - Auto-open: Sheet opens when Ghostwriter completes (currentDraft populated) - Thumbs Up: Copies to clipboard, marks draft as completed, closes sheet - Thumbs Down: Closes sheet, adds feedback prompt to chat - Close button: Closes sheet without changing draft status - Escape key: Closes sheet on desktop - Backdrop click: Closes sheet on desktop **Edge Cases:** - Very long draft (>2000 words): Scrolling works, actions remain accessible - Draft with code blocks: Markdown renders correctly with syntax highlighting - Draft with no tags: Tag section doesn't break - Draft with special characters: Emojis, unicode render correctly - Empty draft: Shows placeholder or error state **Accessibility Tests:** - Keyboard navigation: Tab through actions, Enter to activate - Screen reader: ARIA labels on all buttons - Focus trap: Focus stays in sheet when open - Focus management: Focus returns to chat trigger after close - Color contrast: All text passes WCAG AA (4.5:1) ### Component Implementation Details **DraftViewSheet Component:** ```typescript // src/components/features/draft/DraftViewSheet.tsx import { Sheet, SheetContent, SheetHeader } from '@/components/ui/sheet'; import { DraftContent } from './DraftContent'; import { DraftActions } from './DraftActions'; interface DraftViewSheetProps { draft: Draft | null; open: boolean; onClose: () => void; onApprove: (draftId: string) => void; onReject: (draftId: string) => void; } export function DraftViewSheet({ draft, open, onClose, onApprove, onReject }: DraftViewSheetProps) { if (!draft) return null; return ( {/* Close button, Title */} onApprove(draft.id)} onReject={() => onReject(draft.id)} /> ); } ``` **DraftContent Component:** ```typescript // src/components/features/draft/DraftContent.tsx import ReactMarkdown from 'react-markdown'; interface DraftContentProps { draft: Draft; } export function DraftContent({ draft }: DraftContentProps) { return (

{draft.title}

{draft.content} {draft.tags && draft.tags.length > 0 && (
{draft.tags.map(tag => ( #{tag} ))}
)}
); } ``` **DraftActions Component:** ```typescript // src/components/features/draft/DraftActions.tsx import { Button } from '@/components/ui/button'; import { ThumbsUp, ThumbsDown } from 'lucide-react'; interface DraftActionsProps { onApprove: () => void; onReject: () => void; } export function DraftActions({ onApprove, onReject }: DraftActionsProps) { return (
); } ``` ### Markdown Rendering Strategy **Library Choice:** - Use `react-markdown` for Markdown parsing and rendering - Add `remark-gfm` for GitHub Flavored Markdown support (tables, strikethrough) - Add `rehype-highlight` for syntax highlighting in code blocks **Styling Approach:** - Use Tailwind's `@tailwindcss/typography` plugin for prose styling - Override prose styles to match "Morning Mist" theme - Apply Merriweather font to prose elements only - Keep UI elements (buttons, tags) in Inter font **Configuration:** ```javascript // tailwind.config.js module.exports = { theme: { extend: { fontFamily: { sans: ['Inter', 'sans-serif'], serif: ['Merriweather', 'serif'], }, }, }, plugins: [ require('@tailwindcss/typography'), ], }; ``` ### Clipboard Copy Implementation **Utility Function:** ```typescript // src/lib/utils/clipboard.ts export async function copyToClipboard(text: string): Promise { try { await navigator.clipboard.writeText(text); return true; } catch (err) { // Fallback for older browsers const textArea = document.createElement('textarea'); textArea.value = text; document.body.appendChild(textArea); textArea.select(); try { document.execCommand('copy'); document.body.removeChild(textArea); return true; } catch (fallbackErr) { document.body.removeChild(textArea); return false; } } } ``` **Success Feedback:** - Use ShadCN `useToast()` hook for success notification - Toast message: "Copied to clipboard!" - Optional: Add confetti animation using `canvas-confetti` library ### Service Layer Extensions **DraftService Updates:** ```typescript // src/services/draft-service.ts export class DraftService { // ... existing methods from Story 2.1 async updateDraftStatus( draftId: string, status: 'draft' | 'completed' | 'regenerated' ): Promise { await db.drafts.update(draftId, { status }); } } ``` **ChatService Orchestration:** ```typescript // src/services/chat-service.ts export class ChatService { async approveDraft(draftId: string): Promise { // Update draft status to completed await DraftService.updateDraftStatus(draftId, 'completed'); // Copy to clipboard const draft = await DraftService.getDraftById(draftId); const markdown = this.formatDraftAsMarkdown(draft); await copyToClipboard(markdown); } rejectDraft(draftId: string): void { // Just close the view and return to chat // Story 2.3 will handle the regeneration flow } private formatDraftAsMarkdown(draft: Draft): string { return `# ${draft.title}\n\n${draft.content}\n\nTags: ${draft.tags.join(', ')}`; } } ``` ### Project Structure Notes **Following Feature-First Lite Pattern:** - New feature folder: `src/components/features/draft/` - Draft components co-located for easy discovery - Shares `src/components/ui/` ShadCN primitives **Alignment with Unified Project Structure:** - All feature code under `src/components/features/` - Services orchestrate logic, don't touch DB directly from UI - State managed centrally in Zustand stores - Utilities in `src/lib/utils/` **No Conflicts Detected:** - DraftViewSheet is new UI layer, no conflicts with existing code - Extends existing DraftService with status update method - Adds to ChatStore state, following established patterns ### Performance Requirements **NFR-02 Compliance (App Load Time):** - DraftViewSheet should animate smoothly (< 300ms transition) - Markdown rendering should not block the main thread - Use React.memo for DraftContent to prevent unnecessary re-renders **Rendering Performance:** - Large drafts (>2000 words) should render without jank - Code blocks with syntax highlighting should load quickly - Lazy load syntax highlighting library only when needed **Memory Management:** - Clear draft from currentDraft state after approval/rejection - Don't keep multiple drafts in memory simultaneously - IndexedDB is the source of truth, not component state ### Accessibility Requirements **WCAG AA Compliance:** - Color contrast: All text meets 4.5:1 ratio (Morning Mist palette enforces this) - Touch targets: Minimum 44px for all buttons - Keyboard navigation: Full keyboard support (Tab, Enter, Escape) - Screen reader: ARIA labels on all interactive elements - Focus management: Focus trap in sheet, proper focus return on close **Focus Management:** - When sheet opens, focus moves to close button - Focus is trapped inside sheet while open - When sheet closes, focus returns to trigger element - Thumbs Up/Down buttons are keyboard accessible **Semantic HTML:** - Use `
` for draft content - Use `