- 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>
776 lines
27 KiB
Markdown
776 lines
27 KiB
Markdown
# Story 2.4: Export & Copy Actions
|
|
|
|
Status: review
|
|
|
|
<!-- Note: Validation is optional. Run validate-create-story for quality check before dev-story. -->
|
|
|
|
## Story
|
|
|
|
As a user,
|
|
I want to copy the text or save the post,
|
|
So that I can publish it on LinkedIn or save it for later.
|
|
|
|
## Acceptance Criteria
|
|
|
|
1. **Copy to Clipboard on Thumbs Up**
|
|
- Given the user likes the draft
|
|
- When they click "Thumbs Up" or "Copy"
|
|
- Then the full Markdown text is copied to the clipboard
|
|
- And a success toast/animation confirms the action
|
|
|
|
2. **Save Draft as Completed**
|
|
- Given the draft is finalized
|
|
- When the user saves it
|
|
- Then it is marked as "Completed" in the local database
|
|
- And the user is returned to the Home/History screen
|
|
|
|
## Tasks / Subtasks
|
|
|
|
- [x] Implement Clipboard Copy Functionality
|
|
- [x] Create `copyToClipboard()` utility in `src/lib/utils/clipboard.ts`
|
|
- [x] Support copying Markdown formatted text
|
|
- [x] Handle clipboard API errors gracefully
|
|
- [x] Add fallback for older browsers
|
|
|
|
- [x] Create Success Feedback Toast
|
|
- [x] Create `CopySuccessToast.tsx` component in `src/components/features/feedback/`
|
|
- [x] Show confetti or success animation on copy
|
|
- [x] Auto-dismiss after 3 seconds
|
|
- [x] Include haptic feedback on mobile devices
|
|
- [x] Accessible: announce to screen readers
|
|
|
|
- [x] Extend DraftActions with Copy/Save
|
|
- [x] Add "Copy" button variant (separate from Thumbs Up)
|
|
- [x] Wire Thumbs Up to both copy AND save actions
|
|
- [x] Add "Just Copy" option (copy without closing sheet)
|
|
- [x] Add "Save & Close" action
|
|
|
|
- [x] Implement Draft Completion Logic
|
|
- [x] Add `completeDraft(draftId: number)` action to ChatStore
|
|
- [x] Update draft status from 'draft' to 'completed' in IndexedDB
|
|
- [x] Clear `currentDraft` from store after completion
|
|
- [x] Close DraftViewSheet after completion
|
|
- [ ] Navigate to Home/History screen (deferred to Epic 3)
|
|
|
|
- [x] Implement DraftService Completion Method
|
|
- [x] Add `markAsCompleted(draftId: string)` to `src/lib/db/draft-service.ts`
|
|
- [x] Update DraftRecord status in IndexedDB
|
|
- [x] Add `completedAt` timestamp
|
|
- [x] Return updated draft record
|
|
|
|
- [ ] Create History Screen Navigation
|
|
- [ ] Add navigation to Home/History after save
|
|
- [ ] Ensure completed draft appears in history feed
|
|
- [ ] Scroll to newly saved draft in history
|
|
- [ ] Highlight the newly saved entry
|
|
|
|
- [x] Add Copy Accessibility Features
|
|
- [x] Add `aria-label` to all copy buttons
|
|
- [x] Announce "Copied to clipboard" to screen readers
|
|
- [ ] Support keyboard shortcuts (Cmd+C / Ctrl+C) (deferred - enhancement)
|
|
- [ ] Ensure focus management after copy action (deferred - enhancement)
|
|
|
|
- [x] Test Copy & Save End-to-End
|
|
- [x] Unit test: clipboard.copyText() with Markdown (12 tests)
|
|
- [x] Unit test: DraftService.markAsCompleted() updates status (3 tests)
|
|
- [x] Integration test: Thumbs Up -> Copy -> Save flow (DraftViewSheet tests)
|
|
- [x] Integration test: Copy button only (no save) (DraftViewSheet tests)
|
|
- [x] Edge case: Clipboard permission denied (clipboard fallback tests)
|
|
- [x] Edge case: Draft already marked as completed (DraftService tests)
|
|
- [ ] Edge case: Copy with very long draft content (deferred - stress test)
|
|
|
|
- [ ] Test History Integration
|
|
- [ ] Integration test: Completed draft appears in history feed (deferred to Epic 3)
|
|
- [ ] Integration test: Copy action from history view (Epic 3)
|
|
- [ ] Test: Multiple drafts saved in session (deferred to Epic 3)
|
|
|
|
## Dev Notes
|
|
|
|
### Architecture Compliance (CRITICAL)
|
|
|
|
**Logic Sandwich Pattern - DO NOT VIOLATE:**
|
|
- **UI Components** MUST NOT import `src/lib/db` or clipboard utilities directly
|
|
- All completion logic MUST go through `ChatService` layer
|
|
- ChatService then calls `DraftService` for database operations
|
|
- 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 } = useChatStore();
|
|
|
|
// GOOD - Atomic selectors
|
|
const currentDraft = useChatStore(s => s.currentDraft);
|
|
const completeDraft = useChatStore(s => s.completeDraft);
|
|
```
|
|
|
|
**Local-First Data Boundary:**
|
|
- Completed drafts MUST be stored in IndexedDB
|
|
- Status change from 'draft' to 'completed' persists locally
|
|
- Clipboard operations are client-side only (no server interaction)
|
|
- Draft history is queryable from history view (Epic 3)
|
|
|
|
**Edge Runtime Constraint:**
|
|
- This story does NOT require Edge API calls (clipboard is client-side only)
|
|
- All operations happen in the browser for privacy and speed
|
|
|
|
### Architecture Implementation Details
|
|
|
|
**Issue Resolution (Review Fixes):**
|
|
- **Logic Sandwich Violation:** Fixed `ChatStore.completeDraft` to call `ChatService.approveDraft` instead of direct DB acton.
|
|
- **Missing Component:** Created `CopyButton.tsx` and refactored `DraftActions` to use it.
|
|
- **Redundancy:** Consolidated `approveDraft` and `completeDraft` in Store.
|
|
|
|
**Story Purpose:**
|
|
This story implements the **"Export & Completion"** flow - the final step where users copy their polished draft to clipboard and save it to their local history. This completes the core value loop: Vent -> Generate -> Refine -> Export.
|
|
|
|
**State Management Extensions:**
|
|
```typescript
|
|
// Add to ChatStore (src/lib/store/chat-store.ts)
|
|
interface ChatStore {
|
|
// Existing state
|
|
currentDraft: Draft | null;
|
|
|
|
// New actions for this story
|
|
completeDraft: (draftId: string) => Promise<void>;
|
|
copyDraftToClipboard: (draftId: string) => Promise<void>;
|
|
copyAndSaveDraft: (draftId: string) => Promise<void>;
|
|
}
|
|
```
|
|
|
|
**Completion Logic Flow:**
|
|
1. User views draft in DraftViewSheet (Story 2.2)
|
|
2. User taps **Thumbs Up** OR **Copy** button
|
|
3. **Thumbs Up path:**
|
|
- Copy Markdown to clipboard
|
|
- Show success toast with confetti
|
|
- Mark draft as 'completed' in IndexedDB
|
|
- Clear currentDraft from store
|
|
- Close DraftViewSheet
|
|
- Navigate to Home/History
|
|
- Scroll to/highlight newly saved entry
|
|
4. **Copy Only path:**
|
|
- Copy Markdown to clipboard
|
|
- Show success toast
|
|
- Keep sheet open (user can continue viewing)
|
|
|
|
**Clipboard Utility Pattern:**
|
|
```typescript
|
|
// src/lib/utils/clipboard.ts
|
|
export class ClipboardUtil {
|
|
static async copyMarkdown(markdown: string): Promise<boolean> {
|
|
try {
|
|
await navigator.clipboard.writeText(markdown);
|
|
return true;
|
|
} catch (error) {
|
|
// Fallback for older browsers
|
|
return this.fallbackCopy(markdown);
|
|
}
|
|
}
|
|
|
|
private static fallbackCopy(text: string): boolean {
|
|
const textarea = document.createElement('textarea');
|
|
textarea.value = text;
|
|
document.body.appendChild(textarea);
|
|
textarea.select();
|
|
const success = document.execCommand('copy');
|
|
document.body.removeChild(textarea);
|
|
return success;
|
|
}
|
|
}
|
|
```
|
|
|
|
**Draft Completion in Database:**
|
|
```typescript
|
|
// src/lib/db/draft-service.ts
|
|
export class DraftService {
|
|
static async markAsCompleted(draftId: string): Promise<Draft> {
|
|
await db.drafts.update(draftId, {
|
|
status: 'completed',
|
|
completedAt: Date.now()
|
|
});
|
|
return this.getDraftById(draftId);
|
|
}
|
|
}
|
|
```
|
|
|
|
**Files to Create:**
|
|
- `src/lib/utils/clipboard.ts` - Clipboard utility with fallback
|
|
- `src/components/features/feedback/CopySuccessToast.tsx` - Success feedback
|
|
- `src/components/features/draft/CopyButton.tsx` - Standalone copy button
|
|
|
|
**Files to Modify:**
|
|
- `src/lib/store/chat-store.ts` - Add completion actions
|
|
- `src/lib/db/draft-service.ts` - Add markAsCompleted() method
|
|
- `src/services/chat-service.ts` - Add completion orchestration
|
|
- `src/components/features/draft/DraftActions.tsx` - Add Copy/Save buttons
|
|
- `src/app/(main)/page.tsx` - Navigate to history after save (optional scroll)
|
|
|
|
### UX Design Specifications
|
|
|
|
**From UX Design Document:**
|
|
|
|
**Completion Reward Pattern:**
|
|
- Thumbs Up triggers "You're done!" animation
|
|
- Confetti or haptic feedback provides dopamine hit
|
|
- Success toast confirms copy action
|
|
- Auto-navigation to history reinforces "saved to your journal"
|
|
|
|
**Visual Feedback - Success State:**
|
|
```mermaid
|
|
graph TD
|
|
A[User Taps Thumbs Up] --> B[Copy to Clipboard]
|
|
B --> C[Show Success Toast]
|
|
C --> D[Confetti Animation]
|
|
D --> E[Mark as Completed]
|
|
E --> F[Close Sheet]
|
|
F --> G[Navigate to History]
|
|
G --> H[Highlight New Entry]
|
|
```
|
|
|
|
**Toast Design Specifications:**
|
|
- Position: Top-center or bottom-center (mobile)
|
|
- Duration: 3 seconds auto-dismiss
|
|
- Icon: Checkmark or clipboard icon
|
|
- Text: "Copied to clipboard!" or "Draft saved!"
|
|
- Animation: Fade in + slide up
|
|
- Confetti: Subtle particle burst on success
|
|
|
|
**Button Hierarchy:**
|
|
- **Primary (Thumbs Up):** Copy + Save + Close (complete action)
|
|
- **Secondary (Copy Only):** Copy to clipboard, keep sheet open
|
|
- **Tertiary (Close):** Dismiss without saving
|
|
|
|
**Tone and Emotion:**
|
|
- Success state should feel celebratory
|
|
- "You're done, great job!" messaging
|
|
- Reinforces the value of completing the ritual
|
|
|
|
**Micro-interactions:**
|
|
- Thumbs Up should have "spring" animation
|
|
- Haptic feedback on mobile (vibration)
|
|
- Smooth transition to history screen
|
|
- New entry in history has subtle highlight/fade-in
|
|
|
|
### Previous Story Intelligence (from Stories 2.1, 2.2, and 2.3)
|
|
|
|
**Patterns Established (must follow):**
|
|
- **Logic Sandwich Pattern:** UI -> Zustand -> Service -> DB (strictly enforced)
|
|
- **Atomic Selectors:** All state access uses `useChatStore(s => s.field)`
|
|
- **Draft Storage:** Drafts stored in IndexedDB via `drafts` table
|
|
- **DraftViewSheet:** Sheet component that responds to `currentDraft` state
|
|
- **Draft Status Flow:** 'draft' -> 'regenerated' -> 'completed'
|
|
|
|
**Key Files from Previous Stories:**
|
|
- `src/lib/db/draft-service.ts` - Draft CRUD operations (add completion method)
|
|
- `src/lib/store/chat-store.ts` - Has currentDraft, add completion actions
|
|
- `src/components/features/draft/DraftActions.tsx` - Thumbs Up/Down, add Copy/Save
|
|
- `src/components/features/draft/DraftViewSheet.tsx` - Sheet component
|
|
- `src/services/chat-service.ts` - Chat orchestration (add completion flow)
|
|
|
|
**Learnings to Apply:**
|
|
- Story 2.2 established DraftViewSheet auto-opens on currentDraft - clear it to close
|
|
- Story 2.3 established draft versioning - 'completed' status marks the final version
|
|
- Use same success animation pattern from Story 2.2 (shimmer -> reveal)
|
|
- Follow same error handling pattern (retry on clipboard failure)
|
|
- Story 2.1 established DraftRecord structure - add `completedAt` timestamp
|
|
|
|
**Draft Data Structure (extending from Story 2.1):**
|
|
```typescript
|
|
interface DraftRecord {
|
|
id: string;
|
|
sessionId: string;
|
|
title: string;
|
|
content: string; // Markdown formatted
|
|
tags: string[];
|
|
createdAt: number;
|
|
completedAt?: number; // NEW: Set when draft is marked as completed
|
|
status: 'draft' | 'completed' | 'regenerated';
|
|
}
|
|
```
|
|
|
|
**Integration with Refinement (Story 2.3):**
|
|
- If user refined draft, the final version is marked as 'completed'
|
|
- Original draft(s) with status 'regenerated' remain in history
|
|
- Only the latest draft (the one user approved) gets 'completed' status
|
|
- History view (Epic 3) will show all versions, highlighting the completed one
|
|
|
|
### Clipboard Implementation Specifications
|
|
|
|
**Browser Clipboard API:**
|
|
```typescript
|
|
// Modern browsers (Chrome 66+, Firefox 63+, Safari 13.1+)
|
|
navigator.clipboard.writeText(markdown)
|
|
.then(() => showSuccessToast())
|
|
.catch(() => showFallbackMessage());
|
|
```
|
|
|
|
**Fallback for Older Browsers:**
|
|
```typescript
|
|
// Create hidden textarea, select, execCommand('copy')
|
|
// Remove textarea after copy
|
|
// Returns boolean success
|
|
```
|
|
|
|
**Clipboard Permissions:**
|
|
- Clipboard API requires user gesture (button click)
|
|
- No permissions needed for writeText() in active tab
|
|
- May fail in iframes or cross-origin contexts
|
|
|
|
**Accessibility for Copy:**
|
|
```typescript
|
|
<button
|
|
onClick={handleCopy}
|
|
aria-label="Copy draft to clipboard"
|
|
className="..."
|
|
>
|
|
<CopyIcon />
|
|
<span className="sr-only">Copy</span>
|
|
</button>
|
|
|
|
// After copy, announce to screen readers
|
|
useEffect(() => {
|
|
if (copied) {
|
|
announceToScreenReader('Draft copied to clipboard');
|
|
}
|
|
}, [copied]);
|
|
```
|
|
|
|
**Keyboard Shortcut Support:**
|
|
- Detect Cmd+C / Ctrl+C when draft is visible
|
|
- Show tooltip: "Press Cmd+C to copy"
|
|
- Prevent interference with browser default
|
|
|
|
### Testing Requirements
|
|
|
|
**Unit Tests:**
|
|
- `ClipboardUtil.copyMarkdown()` copies text correctly
|
|
- `ClipboardUtil.copyMarkdown()` handles errors gracefully
|
|
- `ClipboardUtil.fallbackCopy()` works for older browsers
|
|
- `DraftService.markAsCompleted()` updates status
|
|
- `DraftService.markAsCompleted()` sets completedAt timestamp
|
|
- `ChatStore.completeDraft()` clears currentDraft
|
|
- `ChatStore.copyDraftToClipboard()` calls ClipboardUtil
|
|
|
|
**Integration Tests:**
|
|
- Full completion flow: Thumbs Up -> Copy -> Save -> Navigate
|
|
- Copy only flow: Copy button -> Toast -> Sheet stays open
|
|
- Draft appears in history after completion
|
|
- Multiple drafts in session show all completed drafts
|
|
- Refinement + completion: Refined draft marked as completed
|
|
|
|
**Edge Cases:**
|
|
- Clipboard permission denied: Show error message
|
|
- Clipboard API unavailable: Use fallback method
|
|
- Draft already completed: Show message, don't duplicate
|
|
- Very long draft content: Should handle within clipboard limits
|
|
- Copy during refinement: Should work (copy current visible draft)
|
|
- Navigate away during copy: Should handle gracefully
|
|
|
|
**Accessibility Tests:**
|
|
- Screen reader announces "Copied to clipboard"
|
|
- Keyboard navigation works for all copy buttons
|
|
- Focus management after copy
|
|
- ARIA labels on all copy buttons
|
|
|
|
**Performance Tests:**
|
|
- Copy action completes within 500ms
|
|
- Success toast appears within 100ms
|
|
- Navigation to history is smooth (< 300ms)
|
|
|
|
### Component Implementation Details
|
|
|
|
**CopySuccessToast Component:**
|
|
```typescript
|
|
// src/components/features/feedback/CopySuccessToast.tsx
|
|
interface CopySuccessToastProps {
|
|
message: string;
|
|
duration?: number;
|
|
onClose: () => void;
|
|
}
|
|
|
|
export function CopySuccessToast({
|
|
message,
|
|
duration = 3000,
|
|
onClose
|
|
}: CopySuccessToastProps) {
|
|
useEffect(() => {
|
|
const timer = setTimeout(onClose, duration);
|
|
return () => clearTimeout(timer);
|
|
}, [duration, onClose]);
|
|
|
|
// Trigger confetti on mount
|
|
useEffect(() => {
|
|
triggerConfetti();
|
|
}, []);
|
|
|
|
return (
|
|
<div className="fixed bottom-4 left-1/2 -translate-x-1/2 ...">
|
|
<CheckIcon className="text-green-500" />
|
|
<span>{message}</span>
|
|
</div>
|
|
);
|
|
}
|
|
```
|
|
|
|
**CopyButton Component:**
|
|
```typescript
|
|
// src/components/features/draft/CopyButton.tsx
|
|
interface CopyButtonProps {
|
|
draftId: string;
|
|
onCopy?: () => void;
|
|
variant?: 'standalone' | 'toolbar';
|
|
}
|
|
|
|
export function CopyButton({
|
|
draftId,
|
|
onCopy,
|
|
variant = 'standalone'
|
|
}: CopyButtonProps) {
|
|
const [copied, setCopied] = useState(false);
|
|
const copyDraftToClipboard = useChatStore(s => s.copyDraftToClipboard);
|
|
|
|
const handleCopy = async () => {
|
|
await copyDraftToClipboard(draftId);
|
|
setCopied(true);
|
|
onCopy?.();
|
|
setTimeout(() => setCopied(false), 2000);
|
|
};
|
|
|
|
return (
|
|
<button
|
|
onClick={handleCopy}
|
|
aria-label={copied ? 'Copied!' : 'Copy to clipboard'}
|
|
className="..."
|
|
>
|
|
{copied ? <CheckIcon /> : <CopyIcon />}
|
|
</button>
|
|
);
|
|
}
|
|
```
|
|
|
|
**ChatService Extensions:**
|
|
```typescript
|
|
// src/services/chat-service.ts
|
|
export class ChatService {
|
|
async completeDraft(draftId: string): Promise<void> {
|
|
// Copy to clipboard
|
|
const draft = await DraftService.getDraftById(draftId);
|
|
await ClipboardUtil.copyMarkdown(draft.content);
|
|
|
|
// Mark as completed in database
|
|
await DraftService.markAsCompleted(draftId);
|
|
|
|
// Update store (clears currentDraft, closes sheet)
|
|
ChatStore.getState().completeDraft(draftId);
|
|
|
|
// Navigate to history
|
|
router.push('/history');
|
|
}
|
|
|
|
async copyDraftOnly(draftId: string): Promise<void> {
|
|
const draft = await DraftService.getDraftById(draftId);
|
|
await ClipboardUtil.copyMarkdown(draft.content);
|
|
// Don't clear currentDraft - keep sheet open
|
|
}
|
|
|
|
async copyAndSaveDraft(draftId: string): Promise<void> {
|
|
// Same as completeDraft but without navigation
|
|
const draft = await DraftService.getDraftById(draftId);
|
|
await ClipboardUtil.copyMarkdown(draft.content);
|
|
await DraftService.markAsCompleted(draftId);
|
|
ChatStore.getState().completeDraft(draftId);
|
|
}
|
|
}
|
|
```
|
|
|
|
**DraftActions Integration:**
|
|
```typescript
|
|
// Update DraftActions component
|
|
const handleThumbsUp = async () => {
|
|
// Full completion flow
|
|
await ChatService.completeDraft(draftId);
|
|
showSuccessToast('Draft saved to history!');
|
|
};
|
|
|
|
const handleCopyOnly = async () => {
|
|
// Copy without closing
|
|
await ChatService.copyDraftOnly(draftId);
|
|
showSuccessToast('Copied to clipboard!');
|
|
};
|
|
```
|
|
|
|
### History Navigation Strategy
|
|
|
|
**After Completion:**
|
|
```typescript
|
|
// Navigate to history with highlight
|
|
router.push({
|
|
pathname: '/history',
|
|
query: { highlight: draftId }
|
|
});
|
|
|
|
// In history page, scroll to highlighted draft
|
|
useEffect(() => {
|
|
if (router.query.highlight) {
|
|
const element = document.getElementById(`draft-${router.query.highlight}`);
|
|
element?.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
element?.classList.add('highlight-pulse');
|
|
}
|
|
}, [router.query.highlight]);
|
|
```
|
|
|
|
**Highlight Animation:**
|
|
```css
|
|
/* In globals.css */
|
|
@keyframes highlight-pulse {
|
|
0% { box-shadow: 0 0 0 0 rgba(100, 116, 139, 0.7); }
|
|
70% { box-shadow: 0 0 0 10px rgba(100, 116, 139, 0); }
|
|
100% { box-shadow: 0 0 0 0 rgba(100, 116, 139, 0); }
|
|
}
|
|
|
|
.highlight-pulse {
|
|
animation: highlight-pulse 1s ease-out;
|
|
}
|
|
```
|
|
|
|
### Service Layer Extensions
|
|
|
|
**DraftService Completion:**
|
|
```typescript
|
|
// src/lib/db/draft-service.ts
|
|
export class DraftService {
|
|
static async markAsCompleted(draftId: string): Promise<Draft> {
|
|
const existing = await this.getDraftById(draftId);
|
|
|
|
if (existing.status === 'completed') {
|
|
// Already completed, just return
|
|
return existing;
|
|
}
|
|
|
|
await db.drafts.update(draftId, {
|
|
status: 'completed',
|
|
completedAt: Date.now()
|
|
});
|
|
|
|
return this.getDraftById(draftId);
|
|
}
|
|
|
|
static async getCompletedDrafts(): Promise<Draft[]> {
|
|
return await db.drafts
|
|
.where('status')
|
|
.equals('completed')
|
|
.reverse()
|
|
.sortBy('completedAt');
|
|
}
|
|
}
|
|
```
|
|
|
|
**ChatStore Actions:**
|
|
```typescript
|
|
// src/lib/store/chat-store.ts
|
|
interface ChatStore {
|
|
// Existing
|
|
currentDraft: Draft | null;
|
|
|
|
// New actions
|
|
completeDraft: (draftId: string) => Promise<void>;
|
|
copyDraftToClipboard: (draftId: string) => Promise<void>;
|
|
}
|
|
|
|
export const useChatStore = create<ChatStore>((set, get) => ({
|
|
// Existing state...
|
|
|
|
completeDraft: async (draftId: string) => {
|
|
// Mark as completed
|
|
await DraftService.markAsCompleted(draftId);
|
|
// Clear from store (closes DraftViewSheet)
|
|
set({ currentDraft: null });
|
|
},
|
|
|
|
copyDraftToClipboard: async (draftId: string) => {
|
|
const draft = await DraftService.getDraftById(draftId);
|
|
await ClipboardUtil.copyMarkdown(draft.content);
|
|
// Don't clear currentDraft - keep sheet open
|
|
},
|
|
}));
|
|
```
|
|
|
|
### Project Structure Notes
|
|
|
|
**Following Feature-First Lite Pattern:**
|
|
- New components in `src/components/features/feedback/` (toast)
|
|
- New components in `src/components/features/draft/` (copy button)
|
|
- Service extensions in existing files
|
|
- Store updates in `src/lib/store/chat-store.ts`
|
|
|
|
**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
|
|
- Utility functions in `src/lib/utils/`
|
|
|
|
**No Conflicts Detected:**
|
|
- Completion is new status value, no conflicts
|
|
- Clipboard utility is new, no conflicts
|
|
- Navigation to history prepares for Epic 3
|
|
|
|
### Performance Requirements
|
|
|
|
**NFR-01 Compliance (Response Latency):**
|
|
- Copy action should complete within 500ms
|
|
- Success toast should appear within 100ms
|
|
- Navigation to history should be smooth (< 300ms)
|
|
|
|
**State Updates:**
|
|
- currentDraft should clear immediately on completion
|
|
- DraftViewSheet should close smoothly
|
|
- History page should render quickly
|
|
|
|
### Accessibility Requirements
|
|
|
|
**WCAG AA Compliance:**
|
|
- Copy buttons must have `aria-label`
|
|
- Success state must be announced to screen readers
|
|
- Focus management: Return focus after copy
|
|
- Keyboard shortcuts supported
|
|
|
|
**Visual Accessibility:**
|
|
- Success toast must be high contrast
|
|
- Avoid color-only indicators (use icons + text)
|
|
- Highlight animation must respect `prefers-reduced-motion`
|
|
|
|
### Security & Privacy Requirements
|
|
|
|
**NFR-03 & NFR-04 Compliance:**
|
|
- Clipboard operations are client-side only
|
|
- No draft content sent to server
|
|
- Completed drafts stay in IndexedDB
|
|
- No external API calls for copy/save
|
|
|
|
### References
|
|
|
|
**Epic Reference:**
|
|
- [Epic 2: "The Magic Mirror" - Ghostwriter & Draft Refinement](file:///home/maximilienmao/Projects/Test01/_bmad-output/planning-artifacts/epics.md#epic-2-the-magic-mirror---ghostwriter--draft-refinement)
|
|
- [Story 2.4: Export & Copy Actions](file:///home/maximilienmao/Projects/Test01/_bmad-output/planning-artifacts/epics.md#story-24-export--copy-actions)
|
|
- FR-07: "Users can 'One-Click Copy' the formatted text to clipboard"
|
|
- FR-09: "Users can edit the generated draft manually before exporting" (future enhancement)
|
|
|
|
**Architecture Documents:**
|
|
- [Project Context: Logic Sandwich](file:///home/maximilienmao/Projects/Test01/_bmad-output/project-context.md#1-the-logic-sandwich-pattern-service-layer)
|
|
- [Project Context: State Management](file:///home/maximilienmao/Projects/Test01/_bmad-output/project-context.md#2-state-management-zustand)
|
|
- [Project Context: Local-First Boundary](file:///home/maximilienmao/Projects/Test01/_bmad-output/project-context.md#3-local-first-data-boundary)
|
|
|
|
**UX Design Specifications:**
|
|
- [UX: Completion Reward Pattern](file:///home/maximilienmao/Projects/Test01/_bmad-output/planning-artifacts/ux-design-specification.md#24-novel-ux-patterns)
|
|
- [UX: Success State](file:///home/maximilienmao/Projects/Test01/_bmad-output/planning-artifacts/ux-design-specification.md#5-completion-if-liked)
|
|
- [UX: Micro-interactions](file:///home/maximilienmao/Projects/Test01/_bmad-output/planning-artifacts/ux-design-specification.md#micro-interactions)
|
|
|
|
**Previous Stories:**
|
|
- [Story 2.1: Ghostwriter Agent & Markdown Generation](file:///home/maximilienmao/Projects/Test01/_bmad-output/implementation-artifacts/2-1-ghostwriter-agent-markdown-generation.md) - Draft data structure
|
|
- [Story 2.2: Draft View UI (The Slide-Up)](file:///home/maximilienmao/Projects/Test01/_bmad-output/implementation-artifacts/2-2-draft-view-ui-the-slide-up.md) - DraftViewSheet and DraftActions
|
|
- [Story 2.3: Refinement Loop (Regeneration)](file:///home/maximilienmao/Projects/Test01/_bmad-output/implementation-artifacts/2-3-refinement-loop-regeneration.md) - Draft versioning pattern
|
|
|
|
## 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/4ce6b396-ff74-42ee-be06-133000333628/scratchpad`
|
|
|
|
### Completion Notes List
|
|
|
|
**Story Analysis Completed:**
|
|
- Extracted story requirements from Epic 2, Story 2.4
|
|
- Analyzed previous Stories 2.1, 2.2, and 2.3 for established patterns
|
|
- Reviewed architecture for compliance requirements (Logic Sandwich, State Management, Local-First)
|
|
- Reviewed UX specification for completion reward and success state patterns
|
|
- Identified all files to create and modify
|
|
|
|
**Implementation Context Summary:**
|
|
|
|
**Story Purpose:**
|
|
This story implements the **"Export & Completion"** flow - the final step where users copy their polished draft to clipboard and save it to their local history. This completes the core value loop: Vent -> Generate -> Refine -> Export -> Save.
|
|
|
|
**Key Technical Decisions:**
|
|
1. **Clipboard Utility:** Create reusable ClipboardUtil with fallback for older browsers
|
|
2. **Completion Status:** Add 'completed' status to DraftRecord with completedAt timestamp
|
|
3. **Success Feedback:** CopySuccessToast with confetti animation for reward
|
|
4. **Two Copy Modes:** (a) Thumbs Up = copy + save + close, (b) Copy Only = copy without closing
|
|
5. **Navigation:** Auto-navigate to history with highlight after completion
|
|
6. **Haptic Feedback:** Vibration on mobile for tactile confirmation
|
|
|
|
**Dependencies:**
|
|
- No new external dependencies required
|
|
- Uses browser Clipboard API with fallback
|
|
- Reuses existing Zustand, Dexie infrastructure
|
|
- May need confetti library (canvas-confetti) for celebration effect
|
|
|
|
**Integration Points:**
|
|
- Thumbs Up in DraftActions (Story 2.2) triggers ChatService.completeDraft()
|
|
- Completion updates DraftRecord status in IndexedDB
|
|
- Clearing currentDraft closes DraftViewSheet (auto-close pattern from Story 2.2)
|
|
- Navigation to history prepares for Epic 3 implementation
|
|
|
|
**Files to Create:**
|
|
- `src/lib/utils/clipboard.ts` - Clipboard utility with fallback
|
|
- `src/components/features/feedback/CopySuccessToast.tsx` - Success feedback component
|
|
- `src/components/features/draft/CopyButton.tsx` - Standalone copy button
|
|
|
|
**Files to Modify:**
|
|
- `src/lib/store/chat-store.ts` - Add completion actions (completeDraft, copyDraftToClipboard)
|
|
- `src/lib/db/draft-service.ts` - Add markAsCompleted() method
|
|
- `src/services/chat-service.ts` - Add completion orchestration
|
|
- `src/components/features/draft/DraftActions.tsx` - Wire Thumbs Up to completion flow
|
|
|
|
**Testing Strategy:**
|
|
- Unit tests for clipboard utility (including fallback)
|
|
- Integration tests for full completion flow
|
|
- Edge case tests (clipboard denied, already completed, very long content)
|
|
- Accessibility tests (screen reader announcements, keyboard navigation)
|
|
|
|
**Draft Completion Pattern:**
|
|
- Thumbs Up triggers: Copy -> Toast -> Mark Completed -> Clear State -> Navigate
|
|
- Copy Only triggers: Copy -> Toast (no state change)
|
|
- Completed draft gets status='completed' and completedAt timestamp
|
|
- History view (Epic 3) will query by completedAt for reverse chronological order
|
|
|
|
**User Experience Flow:**
|
|
```
|
|
DraftViewSheet (Story 2.2)
|
|
↓
|
|
User taps Thumbs Up
|
|
↓
|
|
Clipboard.copy() - Copy Markdown to clipboard
|
|
↓
|
|
CopySuccessToast - "Copied to clipboard!" + confetti
|
|
↓
|
|
DraftService.markAsCompleted() - Update IndexedDB
|
|
↓
|
|
ChatStore.completeDraft() - Clear currentDraft (closes sheet)
|
|
↓
|
|
Navigate to /history with ?highlight=draftId
|
|
↓
|
|
History scrolls to and highlights the new entry
|
|
```
|
|
|
|
### File List
|
|
|
|
**New Files to Create:**
|
|
- `src/lib/utils/clipboard.ts` - Clipboard utility with fallback support
|
|
- `src/components/features/feedback/CopySuccessToast.tsx` - Success toast with confetti
|
|
- `src/components/features/draft/CopyButton.tsx` - Standalone copy button
|
|
- `src/lib/utils/clipboard.test.ts` - Clipboard utility tests
|
|
- `src/integration/export-copy-actions.test.ts` - End-to-end completion flow tests
|
|
|
|
**Files to Modify:**
|
|
- `src/lib/store/chat-store.ts` - Add completeDraft, copyDraftToClipboard actions
|
|
- `src/lib/db/draft-service.ts` - Add markAsCompleted() method and getCompletedDrafts()
|
|
- `src/lib/db/draft-service.test.ts` - Add completion method tests
|
|
- `src/services/chat-service.ts` - Add completion orchestration (completeDraft, copyDraftOnly)
|
|
- `src/components/features/draft/DraftActions.tsx` - Wire Thumbs Up and Copy button to completion
|