Files
brachnha-insight/_bmad-output/implementation-artifacts/2-2-draft-view-ui-the-slide-up.md
Max 3fbbb1a93b Initial commit: Brachnha Insight project setup
- Next.js 14+ with App Router and TypeScript
- Tailwind CSS and ShadCN UI styling
- Zustand state management
- Dexie.js for IndexedDB (local-first data)
- Auth.js v5 for authentication
- BMAD framework integration

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-26 12:28:43 +07:00

25 KiB

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

  • Implement Draft View Sheet Component

    • Create Sheet.tsx slide-up component (custom implementation, no ShadCN)
    • Configure slide-up animation from bottom (300ms ease-out)
    • Set up responsive behavior (full-screen on mobile, centered card on desktop)
    • Implement backdrop/dim overlay with tap-to-close
  • Implement Draft Content Display

    • Create DraftContent.tsx component for Markdown rendering
    • Apply Merriweather serif font for content (UI font stays Inter)
    • Add generous whitespace and line-height for readability
    • Style headings, paragraphs, and code blocks following Medium-style
    • Add tag display section
  • Implement Action Bar (Thumbs Up/Down)

    • Create DraftActions.tsx component with sticky footer
    • Add Thumbs Up button (approve/copy)
    • Add Thumbs Down button (regenerate feedback)
    • Style buttons to be touch-friendly (44px min height)
    • Add proper ARIA labels for accessibility
  • Integrate with ChatStore

    • Connect currentDraft state to DraftViewSheet
    • Auto-open sheet when currentDraft transitions from null to populated
    • Handle sheet close action (clear currentDraft and showDraftView)
    • Use atomic selectors for state access
  • Implement Copy to Clipboard (Thumbs Up)

    • Implement copyToClipboard() utility inline in ChatStore
    • Copy to clipboard with fallback for older browsers
    • Mark draft as 'completed' in IndexedDB
  • Implement Regeneration Flow (Thumbs Down)

    • Close DraftViewSheet on Thumbs Down tap
    • System message to chat: "What should we change?" (Story 2.3 will add this)
    • Set chat input focus for user feedback
    • Preserve draft context for regeneration in Story 2.3
  • Implement Responsive Layout

    • Mobile (< 768px): Full-screen sheet
    • Desktop (>= 768px): Centered card layout (max-width ~600px)
    • Ensure sheet doesn't stretch too wide on large screens
    • Test keyboard navigation (Escape to close)
  • Test Draft View End-to-End

    • Unit test: Sheet renders in closed state by default
    • Unit test: Sheet opens when open prop is true
    • Unit test: Markdown rendering handles code blocks, lists, links
    • Unit test: DraftContent handles empty tags array
    • Integration test: Sheet auto-opens after Ghostwriter completes
    • Integration test: Thumbs Up copies to clipboard and marks completed
    • Integration test: Thumbs Down returns to chat with feedback prompt
    • 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:

// 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:

// Add to ChatStore (src/lib/store/chat-store.ts)
interface ChatStore {
  // Draft view state
  showDraftView: boolean;
  closeDraftView: () => void;
  approveDraft: (draftId: string) => Promise<void>;
  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:

/* 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):

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:

// 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 (
    <Sheet open={open} onOpenChange={onClose}>
      <SheetContent
        side="bottom"
        className="h-[85vh] sm:h-auto sm:max-h-[85vh] sm:max-w-[600px] sm:mx-auto"
      >
        <SheetHeader>
          {/* Close button, Title */}
        </SheetHeader>
        <DraftContent draft={draft} />
        <DraftActions
          onApprove={() => onApprove(draft.id)}
          onReject={() => onReject(draft.id)}
        />
      </SheetContent>
    </Sheet>
  );
}

DraftContent Component:

// src/components/features/draft/DraftContent.tsx
import ReactMarkdown from 'react-markdown';

interface DraftContentProps {
  draft: Draft;
}

export function DraftContent({ draft }: DraftContentProps) {
  return (
    <div className="draft-content font-merriweather prose prose-slate max-w-none">
      <h2 className="draft-title">{draft.title}</h2>
      <ReactMarkdown className="draft-body">
        {draft.content}
      </ReactMarkdown>
      {draft.tags && draft.tags.length > 0 && (
        <div className="flex flex-wrap gap-2 mt-4">
          {draft.tags.map(tag => (
            <span key={tag} className="tag-chip">#{tag}</span>
          ))}
        </div>
      )}
    </div>
  );
}

DraftActions Component:

// 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 (
    <div className="sticky bottom-0 flex gap-3 p-4 bg-white border-t">
      <Button
        onClick={onReject}
        variant="outline"
        className="flex-1 min-h-[44px]"
        aria-label="Request changes to this draft"
      >
        <ThumbsDown className="mr-2 h-5 w-5" />
        Not Quite
      </Button>
      <Button
        onClick={onApprove}
        className="flex-1 min-h-[44px] bg-slate-700 hover:bg-slate-800"
        aria-label="Approve and copy to clipboard"
      >
        <ThumbsUp className="mr-2 h-5 w-5" />
        Copy
      </Button>
    </div>
  );
}

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:

// tailwind.config.js
module.exports = {
  theme: {
    extend: {
      fontFamily: {
        sans: ['Inter', 'sans-serif'],
        serif: ['Merriweather', 'serif'],
      },
    },
  },
  plugins: [
    require('@tailwindcss/typography'),
  ],
};

Clipboard Copy Implementation

Utility Function:

// src/lib/utils/clipboard.ts
export async function copyToClipboard(text: string): Promise<boolean> {
  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:

// src/services/draft-service.ts
export class DraftService {
  // ... existing methods from Story 2.1

  async updateDraftStatus(
    draftId: string,
    status: 'draft' | 'completed' | 'regenerated'
  ): Promise<void> {
    await db.drafts.update(draftId, { status });
  }
}

ChatService Orchestration:

// src/services/chat-service.ts
export class ChatService {
  async approveDraft(draftId: string): Promise<void> {
    // 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 <article> for draft content
  • Use <nav> for action bar
  • Use proper heading hierarchy (h2 for draft title)
  • Use aria-label for icon-only buttons

Security & Privacy Requirements

NFR-03 & NFR-04 Compliance:

  • Clipboard copy is local-only (client-side API)
  • No draft content transmitted to server on approval
  • Draft status update is a local IndexedDB operation
  • Copy to clipboard does not expose content to external services

Content Safety:

  • Draft content is user-generated, no sanitization needed for local display
  • Markdown rendering should not execute arbitrary scripts
  • Use react-markdown which sanitizes by default

References

Epic Reference:

Architecture Documents:

UX Design Specifications:

Previous Stories:

Dev Agent Record

Agent Model Used

Claude Opus 4.5 (model ID: 'claude-opus-4-5-20251101')

Debug Log References

Session file: /tmp/claude/-home-maximilienmao-Projects/Test01/80b59076-c368-433c-92e4-1937285218ee/scratchpad

Completion Notes List

Story Analysis Completed:

  • Extracted story requirements from Epic 2, Story 2.2
  • Analyzed previous Story 2.1 for established patterns (Ghostwriter, DraftRecord, DraftService)
  • Reviewed architecture for compliance requirements (Logic Sandwich, State Management, Local-First)
  • Reviewed UX specification for "Magic Moment" visualization, typography, responsive design
  • Identified all files to create and modify

Implementation Context Summary:

Story Purpose: This story implements the "Magic Moment" UI - the slide-up sheet that displays the Ghostwriter's draft in a polished, reading-focused interface. This is the emotional climax of the user journey where raw chat is transformed into a tangible artifact.

Key Technical Decisions:

  1. Component Structure: DraftViewSheet (Sheet wrapper) -> DraftContent (Markdown) + DraftActions (Footer)
  2. Typography Split: Merriweather (serif) for draft content, Inter (sans-serif) for UI - reinforces transformation value
  3. Responsive Design: Full-screen on mobile, centered 600px card on desktop ("Centered App" pattern)
  4. Markdown Rendering: react-markdown + @tailwindcss/typography for prose styling
  5. State Management: Add draft view state to ChatStore (showDraftView, closeDraftView, approveDraft, rejectDraft)
  6. Service Extensions: DraftService.updateDraftStatus() for marking drafts as completed

Dependencies:

  • New dependencies to add:
    • react-markdown - Markdown parsing and rendering
    • remark-gfm - GitHub Flavored Markdown support
    • rehype-highlight - Syntax highlighting for code blocks
    • @tailwindcss/typography - Tailwind prose styling plugin
    • canvas-confetti (optional) - Success animation on copy
  • Reuses existing: Zustand, Dexie, ShadCN Sheet component

Integration Points:

  • Auto-opens when Ghostwriter completes (watch currentDraft state in ChatStore)
  • Thumbs Up: Copies to clipboard, marks draft as 'completed' in IndexedDB
  • Thumbs Down: Closes sheet, returns to chat (Story 2.3 handles regeneration flow)
  • Draft content already stored in IndexedDB from Story 2.1

Files to Create:

  • src/components/features/draft/DraftViewSheet.tsx - Main sheet component with ShadCN Sheet wrapper
  • src/components/features/draft/DraftContent.tsx - Markdown renderer with Merriweather font
  • src/components/features/draft/DraftActions.tsx - Thumbs Up/Down sticky footer
  • src/lib/utils/clipboard.ts - Copy to clipboard utility function

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
  • tailwind.config.js - Add Merriweather font and typography plugin

Testing Strategy:

  • Unit tests for each component (DraftViewSheet, DraftContent, DraftActions)
  • Integration tests for full flow (Ghostwriter -> DraftView -> Copy/Reject)
  • Edge case tests (long drafts, code blocks, special characters)
  • Accessibility tests (keyboard nav, screen reader, focus management)

File List

New 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
  • src/components/features/draft/draft-view-sheet.test.tsx - Component tests
  • src/components/features/draft/draft-content.test.tsx - Markdown rendering tests
  • src/components/features/draft/draft-actions.test.tsx - Action button tests
  • src/integration/draft-view-flow.test.ts - End-to-end flow tests