feat(epic-2): complete Epic 2 story preparation - all stories ready-for-dev

- Created Epic 2 tech spec (tech-spec-epic-2.md)
- Drafted 5 stories for Resume Preview Page:
  * 2.1: Create Resume Page Route
  * 2.2: Create Resume Preview Container Component
  * 2.3: Create Resume Header & Main Content Components
  * 2.4: Create Resume Sidebar Components
  * 2.5: Create Download Button Component
- Generated story context XML files for all stories
- Updated sprint-status.yaml: Epic 2 contexted, all stories ready-for-dev

Epic 2 covers FR5-9, FR15-25 (resume preview with two-column layout,
responsive design, print styles, and ATS-compatible HTML structure)
This commit is contained in:
mahdiarghyani
2025-11-30 16:45:57 +03:30
parent a492b0ba05
commit c60100ac90
12 changed files with 2203 additions and 6 deletions
@@ -0,0 +1,100 @@
<story-context id="2-1-create-resume-page-route" v="1.0">
<metadata>
<epicId>2</epicId>
<storyId>2.1</storyId>
<title>Create Resume Page Route</title>
<status>ready-for-dev</status>
<generatedAt>2025-11-30</generatedAt>
<generator>BMAD Story Context Workflow</generator>
<sourceStoryPath>docs/sprint-artifacts/2-1-create-resume-page-route.md</sourceStoryPath>
</metadata>
<story>
<asA>user</asA>
<iWant>to access my resume at `/resume`</iWant>
<soThat>I can view it before downloading</soThat>
<tasks>
- Create resume page route (AC: #1, #2)
- Configure page metadata (AC: #4, #5)
- Implement print mode detection (AC: #6)
- Add placeholder content (AC: #1)
- Test page functionality
</tasks>
</story>
<acceptanceCriteria>
<criterion id="AC1">Given I navigate to `/resume`, when the page loads, then I see the resume preview</criterion>
<criterion id="AC2">The page is standalone (no site header/footer)</criterion>
<criterion id="AC3">The page has a white background</criterion>
<criterion id="AC4">The page title is "Resume - Ali Arghyani"</criterion>
<criterion id="AC5">Meta tags are set for SEO (noindex for privacy)</criterion>
<criterion id="AC6">Given I add `?print=true` query parameter, when the page loads, then the download button is hidden (for PDF generation)</criterion>
</acceptanceCriteria>
<artifacts>
<docs>
<doc>
<path>docs/architecture.md</path>
<title>Resume Export Feature - Architecture Document</title>
<section>Novel Pattern: WYSIWYG PDF Export</section>
<snippet>Single component renders both web preview and PDF source. Query Parameter: `?print=true` hides download button in PDF.</snippet>
</doc>
<doc>
<path>docs/architecture.md</path>
<title>Resume Export Feature - Architecture Document</title>
<section>Project Structure</section>
<snippet>File: `app/pages/resume.vue` - /resume route (standalone page)</snippet>
</doc>
<doc>
<path>docs/sprint-artifacts/tech-spec-epic-2.md</path>
<title>Epic Technical Specification: Resume Preview Page</title>
<section>AC1: Standalone Resume Route</section>
<snippet>Page is standalone (no site header/footer), page title is "Resume - Ali Arghyani", meta tag `<meta name="robots" content="noindex">` is present</snippet>
</doc>
</docs>
<code>
<!-- No existing code - this is the first story creating the page -->
</code>
<dependencies>
<node>
<package name="nuxt" version="^4.1.3" />
<package name="vue" version="^3.5.13" />
</node>
</dependencies>
</artifacts>
<constraints>
- Use `definePageMeta({ layout: false })` for standalone page
- Use `useHead()` or `useSeoMeta()` for metadata
- Use `useRoute().query.print` for print mode detection
- File location must be `app/pages/resume.vue` (Nuxt file-based routing)
- White background: `bg-white` class
</constraints>
<interfaces>
<interface>
<name>useRoute</name>
<kind>Nuxt composable</kind>
<signature>const route = useRoute(); const isPrintMode = computed(() => route.query.print === 'true')</signature>
<path>nuxt/app</path>
</interface>
<interface>
<name>useHead</name>
<kind>Nuxt composable</kind>
<signature>useHead({ title: string, meta: Array<{ name: string, content: string }> })</signature>
<path>nuxt/app</path>
</interface>
</interfaces>
<tests>
<standards>Nuxt 4 testing with Vitest. Test page routing, metadata, and print mode detection.</standards>
<locations>tests/, app/**/*.spec.ts</locations>
<ideas>
<idea ac="AC1">Navigate to /resume and verify page renders</idea>
<idea ac="AC2">Check that no layout is applied (standalone)</idea>
<idea ac="AC4">Verify page title in document.title</idea>
<idea ac="AC5">Check for noindex meta tag in DOM</idea>
<idea ac="AC6">Test with ?print=true parameter and verify isPrintMode is true</idea>
</ideas>
</tests>
</story-context>
@@ -0,0 +1,151 @@
# Story 2.1: Create Resume Page Route
Status: ready-for-dev
## Story
As a user,
I want to access my resume at `/resume`,
so that I can view it before downloading.
## Acceptance Criteria
1. **AC1:** Given I navigate to `/resume`, when the page loads, then I see the resume preview
2. **AC2:** The page is standalone (no site header/footer)
3. **AC3:** The page has a white background
4. **AC4:** The page title is "Resume - Ali Arghyani"
5. **AC5:** Meta tags are set for SEO (noindex for privacy)
6. **AC6:** Given I add `?print=true` query parameter, when the page loads, then the download button is hidden (for PDF generation)
## Tasks / Subtasks
- [ ] Create resume page route (AC: #1, #2)
- [ ] Create `app/pages/resume.vue` file
- [ ] Use `definePageMeta({ layout: false })` for standalone page
- [ ] Add basic page structure with white background
- [ ] Configure page metadata (AC: #4, #5)
- [ ] Set page title using `useHead()` or `useSeoMeta()`
- [ ] Add `<meta name="robots" content="noindex">` for privacy
- [ ] Ensure title format: "Resume - Ali Arghyani"
- [ ] Implement print mode detection (AC: #6)
- [ ] Use `useRoute().query.print` to detect print parameter
- [ ] Pass print mode state to child components
- [ ] Verify download button visibility logic
- [ ] Add placeholder content (AC: #1)
- [ ] Import `ResumePreview` component (will be created in Story 2.2)
- [ ] For now, add placeholder text: "Resume Preview Coming Soon"
- [ ] Ensure page renders without errors
- [ ] Test page functionality
- [ ] Navigate to `/resume` and verify standalone layout
- [ ] Check page title in browser tab
- [ ] Inspect meta tags in DOM
- [ ] Test `/resume?print=true` parameter handling
- [ ] Verify white background styling
## Dev Notes
### Architecture Alignment
**From Architecture Doc:**
- File location: `app/pages/resume.vue` (confirmed in Project Structure section)
- Layout: Use `definePageMeta({ layout: false })` for standalone page
- Query parameter: `?print=true` hides download button for PDF generation
- SEO: `noindex` meta tag for privacy (resume not indexed by search engines)
**From Tech Spec Epic 2:**
- AC1-AC6 map directly to this story
- Page serves dual purposes: (1) user preview, (2) PDF generation source
- WYSIWYG approach: same page renders for web and PDF
### Project Structure Notes
**File to Create:**
- `app/pages/resume.vue` - Main resume route
**Dependencies:**
- Nuxt 4 routing (file-based)
- `useHead()` or `useSeoMeta()` composable for metadata
- `useRoute()` composable for query parameter detection
**Future Integration:**
- Story 2.2 will create `ResumePreview.vue` component to replace placeholder
- Story 2.5 will create `ResumeDownloadButton.vue` that respects print mode
### Implementation Notes
**Page Structure:**
```vue
<script setup lang="ts">
definePageMeta({
layout: false
})
const route = useRoute()
const isPrintMode = computed(() => route.query.print === 'true')
useHead({
title: 'Resume - Ali Arghyani',
meta: [
{ name: 'robots', content: 'noindex' }
]
})
</script>
<template>
<div class="min-h-screen bg-white">
<!-- Placeholder for ResumePreview component (Story 2.2) -->
<div class="p-8 text-center">
<h1 class="text-2xl font-bold">Resume Preview Coming Soon</h1>
</div>
<!-- Download button will be added in Story 2.5 -->
<!-- Hidden when isPrintMode is true -->
</div>
</template>
```
**Testing Checklist:**
- [ ] Page accessible at `http://localhost:3000/resume`
- [ ] No site navigation visible (header/footer)
- [ ] White background applied
- [ ] Browser tab shows "Resume - Ali Arghyani"
- [ ] Meta tag `<meta name="robots" content="noindex">` present in DOM
- [ ] `?print=true` parameter detected correctly
### References
- [Source: docs/architecture.md#Novel-Pattern-WYSIWYG-PDF-Export]
- [Source: docs/architecture.md#Project-Structure]
- [Source: docs/sprint-artifacts/tech-spec-epic-2.md#AC1-Standalone-Resume-Route]
- [Source: docs/epics.md#Story-2.1-Create-Resume-Page-Route]
## Dev Agent Record
### Context Reference
- docs/sprint-artifacts/2-1-create-resume-page-route.context.xml
### Agent Model Used
<!-- Will be filled by dev agent -->
### Debug Log References
<!-- Will be filled by dev agent during implementation -->
### Completion Notes List
<!-- Will be filled by dev agent after completion -->
### File List
<!-- Will be filled by dev agent with created/modified files -->
---
**Change Log:**
- 2025-11-30: Story drafted by SM agent (mahdi)
@@ -0,0 +1,147 @@
<story-context id="2-2-create-resume-preview-container-component" v="1.0">
<metadata>
<epicId>2</epicId>
<storyId>2.2</storyId>
<title>Create Resume Preview Container Component</title>
<status>ready-for-dev</status>
<generatedAt>2025-11-30</generatedAt>
<generator>BMAD Story Context Workflow</generator>
<sourceStoryPath>docs/sprint-artifacts/2-2-create-resume-preview-container-component.md</sourceStoryPath>
</metadata>
<story>
<asA>developer</asA>
<iWant>a container component that renders the full resume</iWant>
<soThat>I have a single source of truth for web and PDF</soThat>
<tasks>
- Create ResumePreview component file
- Implement two-column layout
- Configure container dimensions
- Apply color scheme and typography
- Add print styles
- Integrate data from composable
- Add placeholder sections
- Test component rendering
</tasks>
</story>
<acceptanceCriteria>
<criterion id="AC1">Two-column layout with left sidebar (35% width) and right main content (65% width)</criterion>
<criterion id="AC2">Container has A4 aspect ratio (210mm × 297mm)</criterion>
<criterion id="AC3">Page margins are 24px (1.5rem)</criterion>
<criterion id="AC4">Background is white</criterion>
<criterion id="AC5">Color scheme is blue (#2563eb) and white</criterion>
<criterion id="AC6">Typography uses Inter font for English text</criterion>
<criterion id="AC7">Proper heading hierarchy (h1 for name, h2 for sections)</criterion>
<criterion id="AC8">Body text is 14px (0.875rem)</criterion>
<criterion id="AC9">ATS-readable font sizes</criterion>
<criterion id="AC10">Layout is responsive - desktop shows two-column side by side, mobile shows single column (sidebar on top)</criterion>
<criterion id="AC11">Print styles included - `.no-print` class hides elements in print</criterion>
<criterion id="AC12">Page breaks are controlled</criterion>
<criterion id="AC13">Colors print correctly (`printBackground: true`)</criterion>
</acceptanceCriteria>
<artifacts>
<docs>
<doc>
<path>docs/architecture.md</path>
<title>Resume Export Feature - Architecture Document</title>
<section>Consistency Rules - Color Scheme</section>
<snippet>Primary (headers, icons): Blue - text-blue-600, bg-blue-600. Background: White - bg-white. Text: Dark gray - text-gray-800.</snippet>
</doc>
<doc>
<path>docs/architecture.md</path>
<title>Resume Export Feature - Architecture Document</title>
<section>Consistency Rules - Typography</section>
<snippet>Name: Inter, 2rem, Bold. Section Headers: Inter, 1rem, Semibold. Body Text: Inter, 0.875rem, Normal.</snippet>
</doc>
<doc>
<path>docs/architecture.md</path>
<title>Resume Export Feature - Architecture Document</title>
<section>Consistency Rules - Spacing</section>
<snippet>Page margins: 1.5rem (24px). Section gap: 1.5rem. Sidebar width: 35%. Main content width: 65%.</snippet>
</doc>
<doc>
<path>docs/sprint-artifacts/tech-spec-epic-2.md</path>
<title>Epic Technical Specification: Resume Preview Page</title>
<section>AC2: Two-Column Layout</section>
<snippet>Two-column grid: Left sidebar (35% width): Contact, Skills, Education, Languages. Right main content (65% width): Header, Summary, Experience. Container has A4 aspect ratio (210mm × 297mm).</snippet>
</doc>
</docs>
<code>
<artifact>
<path>app/composables/useResumeData.ts</path>
<kind>composable</kind>
<symbol>useResumeData</symbol>
<reason>Provides reactive access to resume data for this component</reason>
</artifact>
<artifact>
<path>app/types/resume.ts</path>
<kind>types</kind>
<symbol>Resume, ResumeBasics, WorkExperience, Education, Skill, Language</symbol>
<reason>TypeScript interfaces for resume data structure</reason>
</artifact>
<artifact>
<path>app/data/resume.en.ts</path>
<kind>data</kind>
<symbol>resumeData</symbol>
<reason>Sample resume data to display</reason>
</artifact>
<artifact>
<path>app/pages/resume.vue</path>
<kind>page</kind>
<symbol>resume page</symbol>
<reason>Will import and render this ResumePreview component</reason>
</artifact>
</code>
<dependencies>
<node>
<package name="nuxt" version="^4.1.3" />
<package name="@nuxt/ui" version="^4.0.x" />
<package name="tailwindcss" version="^4.1.x" />
<package name="@nuxt/fonts" version="^0.11.x" />
</node>
</dependencies>
</artifacts>
<constraints>
- Use CSS Grid for two-column layout
- Tailwind classes for all styling
- A4 dimensions: max-w-[210mm] min-h-[297mm]
- Margins: p-6 (1.5rem = 24px)
- Responsive breakpoint: md:grid-cols-[35%_65%]
- Print styles: @media print with .no-print class
- Color palette: Blue #2563eb, White #ffffff, Gray #1f2937
- Typography: text-sm (0.875rem) for body, text-base for headers
- File location: app/components/resume/ResumePreview.vue
</constraints>
<interfaces>
<interface>
<name>useResumeData</name>
<kind>composable</kind>
<signature>const { resume, formatDate, getFullName, getPdfFilename } = useResumeData()</signature>
<path>app/composables/useResumeData.ts</path>
</interface>
<interface>
<name>Resume</name>
<kind>TypeScript interface</kind>
<signature>interface Resume { basics: ResumeBasics; work: WorkExperience[]; education: Education[]; skills: Skill[]; languages?: Language[] }</signature>
<path>app/types/resume.ts</path>
</interface>
</interfaces>
<tests>
<standards>Nuxt 4 component testing with Vitest. Test layout, responsive behavior, and print styles.</standards>
<locations>app/components/**/*.spec.ts</locations>
<ideas>
<idea ac="AC1">Mount component and verify two-column grid structure</idea>
<idea ac="AC2">Check container dimensions match A4 (210mm × 297mm)</idea>
<idea ac="AC3">Verify padding is 1.5rem (24px)</idea>
<idea ac="AC4">Check background color is white</idea>
<idea ac="AC5">Verify blue color (#2563eb) is applied</idea>
<idea ac="AC10">Test responsive behavior at mobile breakpoint</idea>
<idea ac="AC11">Verify .no-print class exists in styles</idea>
</ideas>
</tests>
</story-context>
@@ -0,0 +1,232 @@
# Story 2.2: Create Resume Preview Container Component
Status: ready-for-dev
## Story
As a developer,
I want a container component that renders the full resume,
so that I have a single source of truth for web and PDF.
## Acceptance Criteria
1. **AC1:** Given the ResumePreview component is rendered, when it displays, then it shows a two-column layout with left sidebar (35% width) and right main content (65% width)
2. **AC2:** The container has A4 aspect ratio (210mm × 297mm)
3. **AC3:** Page margins are 24px (1.5rem)
4. **AC4:** Background is white
5. **AC5:** Color scheme is blue (#2563eb) and white
6. **AC6:** Typography uses Inter font for English text
7. **AC7:** Proper heading hierarchy (h1 for name, h2 for sections)
8. **AC8:** Body text is 14px (0.875rem)
9. **AC9:** ATS-readable font sizes
10. **AC10:** Layout is responsive - desktop shows two-column side by side, mobile shows single column (sidebar on top)
11. **AC11:** Print styles included - `.no-print` class hides elements in print
12. **AC12:** Page breaks are controlled
13. **AC13:** Colors print correctly (`printBackground: true`)
## Tasks / Subtasks
- [ ] Create ResumePreview component file (AC: #1-#13)
- [ ] Create `app/components/resume/ResumePreview.vue`
- [ ] Set up component structure with script setup and TypeScript
- [ ] Implement two-column layout (AC: #1, #10)
- [ ] Use CSS Grid for two-column layout
- [ ] Set sidebar width to 35%, main content to 65%
- [ ] Add responsive breakpoint for mobile (single column)
- [ ] Ensure sidebar appears above main content on mobile
- [ ] Configure container dimensions (AC: #2, #3)
- [ ] Set container to A4 aspect ratio (210mm × 297mm)
- [ ] Apply 24px (1.5rem) margins
- [ ] Use Tailwind classes: `max-w-[210mm] min-h-[297mm] p-6`
- [ ] Apply color scheme and typography (AC: #4-#9)
- [ ] Set white background: `bg-white`
- [ ] Define blue primary color: `text-blue-600` (#2563eb)
- [ ] Configure Inter font (already available via @nuxt/fonts)
- [ ] Set body text size: `text-sm` (0.875rem)
- [ ] Ensure proper heading hierarchy (h1, h2)
- [ ] Add print styles (AC: #11-#13)
- [ ] Create `.no-print` utility class
- [ ] Add `@media print` styles
- [ ] Set `printBackground: true` for colors
- [ ] Control page breaks with `break-inside-avoid`
- [ ] Integrate data from composable
- [ ] Import `useResumeData()` composable
- [ ] Access `resume` reactive reference
- [ ] Prepare for child component integration (Stories 2.3, 2.4)
- [ ] Add placeholder sections
- [ ] Add comment placeholders for child components
- [ ] Structure: Sidebar (Contact, Skills, Education, Languages)
- [ ] Structure: Main (Header, Summary, Experience)
- [ ] Test component rendering
- [ ] Import component in `pages/resume.vue`
- [ ] Verify two-column layout on desktop
- [ ] Test responsive behavior on mobile
- [ ] Check print preview (Ctrl+P)
- [ ] Verify A4 dimensions and margins
## Dev Notes
### Architecture Alignment
**From Architecture Doc:**
- File location: `app/components/resume/ResumePreview.vue`
- Layout: CSS Grid for two-column design
- Styling: Tailwind CSS utilities
- Data source: `useResumeData()` composable from Epic 1
**From Tech Spec Epic 2:**
- AC1-AC13 map directly to this story
- Container is the main orchestrator for all resume sections
- WYSIWYG pattern: same component for web and PDF
### Learnings from Previous Story
**From Story 2.1 (Status: drafted)**
- Page route created at `app/pages/resume.vue`
- Standalone layout configured with `definePageMeta({ layout: false })`
- Print mode detection implemented via `useRoute().query.print`
- White background and metadata already set
- **Integration Point**: Import `ResumePreview` component to replace placeholder
[Source: docs/sprint-artifacts/2-1-create-resume-page-route.md]
### Project Structure Notes
**File to Create:**
- `app/components/resume/ResumePreview.vue` - Main container component
**Dependencies:**
- `app/composables/useResumeData.ts` (Epic 1 - completed)
- `app/types/resume.ts` (Epic 1 - completed)
- `app/data/resume.en.ts` (Epic 1 - completed)
- Tailwind CSS (existing)
- @nuxt/fonts with Inter (existing)
**Future Integration:**
- Story 2.3 will create Header, Summary, Experience components
- Story 2.4 will create Contact, Skills, Education, Languages components
- Story 2.5 will create Download Button component
### Implementation Notes
**Component Structure:**
```vue
<script setup lang="ts">
const { resume } = useResumeData()
</script>
<template>
<div class="min-h-screen bg-gray-50 flex items-center justify-center p-8">
<!-- A4 Container -->
<div class="bg-white shadow-lg max-w-[210mm] min-h-[297mm] w-full">
<!-- Two-column grid -->
<div class="grid grid-cols-1 md:grid-cols-[35%_65%] gap-0 p-6">
<!-- Left Sidebar -->
<div class="bg-blue-50 p-6 space-y-6">
<!-- Contact (Story 2.4) -->
<!-- Skills (Story 2.4) -->
<!-- Education (Story 2.4) -->
<!-- Languages (Story 2.4) -->
</div>
<!-- Right Main Content -->
<div class="p-6 space-y-6">
<!-- Header (Story 2.3) -->
<!-- Summary (Story 2.3) -->
<!-- Experience (Story 2.3) -->
</div>
</div>
</div>
</div>
</template>
<style scoped>
@media print {
.no-print {
display: none !important;
}
.resume-container {
width: 210mm;
min-height: 297mm;
box-shadow: none;
}
* {
print-color-adjust: exact;
-webkit-print-color-adjust: exact;
}
}
</style>
```
**Responsive Breakpoints:**
- Desktop (md: 768px+): Two columns side by side
- Mobile (< 768px): Single column, sidebar stacked on top
**Color Palette:**
- Primary Blue: `#2563eb` (text-blue-600, bg-blue-600)
- Background: `#ffffff` (bg-white)
- Sidebar Background: `#eff6ff` (bg-blue-50)
- Text: `#1f2937` (text-gray-800)
- Secondary Text: `#6b7280` (text-gray-600)
**Typography Scale:**
- Name (h1): 2rem (text-3xl), font-bold
- Section Headers (h2): 1rem (text-base), font-semibold
- Body Text: 0.875rem (text-sm), font-normal
**Testing Checklist:**
- [ ] Component renders without errors
- [ ] Two-column layout displays correctly on desktop
- [ ] Single column layout on mobile (< 768px)
- [ ] A4 dimensions maintained (210mm × 297mm)
- [ ] Margins are 24px (1.5rem)
- [ ] White background applied
- [ ] Blue color scheme visible
- [ ] Inter font loaded and applied
- [ ] Print preview shows correct styling
- [ ] `.no-print` class works in print mode
### References
- [Source: docs/architecture.md#Project-Structure]
- [Source: docs/architecture.md#Novel-Pattern-WYSIWYG-PDF-Export]
- [Source: docs/architecture.md#Consistency-Rules]
- [Source: docs/sprint-artifacts/tech-spec-epic-2.md#AC2-Two-Column-Layout]
- [Source: docs/sprint-artifacts/tech-spec-epic-2.md#Detailed-Design]
- [Source: docs/epics.md#Story-2.2-Create-Resume-Preview-Container-Component]
## Dev Agent Record
### Context Reference
- docs/sprint-artifacts/2-2-create-resume-preview-container-component.context.xml
### Agent Model Used
<!-- Will be filled by dev agent -->
### Debug Log References
<!-- Will be filled by dev agent during implementation -->
### Completion Notes List
<!-- Will be filled by dev agent after completion -->
### File List
<!-- Will be filled by dev agent with created/modified files -->
---
**Change Log:**
- 2025-11-30: Story drafted by SM agent (mahdi)
@@ -0,0 +1,131 @@
<story-context id="2-3-create-resume-header-main-content-components" v="1.0">
<metadata>
<epicId>2</epicId>
<storyId>2.3</storyId>
<title>Create Resume Header & Main Content Components</title>
<status>ready-for-dev</status>
<generatedAt>2025-11-30</generatedAt>
<generator>BMAD Story Context Workflow</generator>
<sourceStoryPath>docs/sprint-artifacts/2-3-create-resume-header-main-content-components.md</sourceStoryPath>
</metadata>
<story>
<asA>user</asA>
<iWant>to see my name, title, summary, and work experience prominently</iWant>
<soThat>recruiters see the most important information first</soThat>
<tasks>
- Create ResumeHeader component
- Create ResumeSummary component
- Create ResumeExperience component
- Integrate components into ResumePreview
- Test components rendering
</tasks>
</story>
<acceptanceCriteria>
<criterion id="AC1">Header shows full name in large bold text (2rem, font-bold)</criterion>
<criterion id="AC2">Job title appears below name (1.25rem, text-gray-600)</criterion>
<criterion id="AC3">Blue accent line or background element is present</criterion>
<criterion id="AC4">Summary shows "Profile" or "Summary" section header</criterion>
<criterion id="AC5">Professional summary paragraph is displayed</criterion>
<criterion id="AC6">Line height is 1.6 for readability</criterion>
<criterion id="AC7">Experience shows for each job: position title (bold), company name, date range (formatted: "Jan 2022 - Present"), bullet points for highlights (• character)</criterion>
<criterion id="AC8">Jobs are sorted by date (most recent first)</criterion>
<criterion id="AC9">"Present" is shown for current jobs (no endDate)</criterion>
</acceptanceCriteria>
<artifacts>
<docs>
<doc>
<path>docs/architecture.md</path>
<title>Resume Export Feature - Architecture Document</title>
<section>Data Architecture</section>
<snippet>WorkExperience interface: company, position, startDate (YYYY-MM), endDate (optional), highlights (string[])</snippet>
</doc>
<doc>
<path>docs/sprint-artifacts/tech-spec-epic-2.md</path>
<title>Epic Technical Specification: Resume Preview Page</title>
<section>AC5: Header Section, AC10: Summary Section, AC11: Experience Section</section>
<snippet>Header: Name in 2rem bold, job title 1.25rem gray, blue accent line. Summary: Section header, paragraph with line-height 1.6. Experience: Position, company, date range, bullet highlights.</snippet>
</doc>
</docs>
<code>
<artifact>
<path>app/components/resume/ResumePreview.vue</path>
<kind>component</kind>
<symbol>ResumePreview</symbol>
<reason>Parent container where these components will be integrated</reason>
</artifact>
<artifact>
<path>app/composables/useResumeData.ts</path>
<kind>composable</kind>
<symbol>useResumeData, formatDate</symbol>
<reason>Provides data and date formatting helper</reason>
</artifact>
<artifact>
<path>app/types/resume.ts</path>
<kind>types</kind>
<symbol>WorkExperience, ResumeBasics</symbol>
<reason>TypeScript interfaces for props</reason>
</artifact>
</code>
<dependencies>
<node>
<package name="vue" version="^3.5.13" />
<package name="@nuxt/ui" version="^4.0.x" />
</node>
</dependencies>
</artifacts>
<constraints>
- Header: text-3xl font-bold for name, text-xl text-gray-600 for title
- Blue accent: border-b-4 border-blue-600
- Summary: text-base font-semibold for header, leading-relaxed (1.6) for paragraph
- Experience: Sort by startDate descending, format dates with formatDate() helper
- Bullet character: • (U+2022)
- File locations: app/components/resume/ResumeHeader.vue, ResumeSummary.vue, ResumeExperience.vue
- Props pattern: Accept specific data fields, not entire resume object
</constraints>
<interfaces>
<interface>
<name>ResumeHeader Props</name>
<kind>Vue component props</kind>
<signature>interface Props { name: string; label: string }</signature>
<path>app/components/resume/ResumeHeader.vue</path>
</interface>
<interface>
<name>ResumeSummary Props</name>
<kind>Vue component props</kind>
<signature>interface Props { summary: string }</signature>
<path>app/components/resume/ResumeSummary.vue</path>
</interface>
<interface>
<name>ResumeExperience Props</name>
<kind>Vue component props</kind>
<signature>interface Props { work: WorkExperience[] }</signature>
<path>app/components/resume/ResumeExperience.vue</path>
</interface>
<interface>
<name>formatDate</name>
<kind>helper function</kind>
<signature>formatDate(date: string, locale?: string): string</signature>
<path>app/composables/useResumeData.ts</path>
</interface>
</interfaces>
<tests>
<standards>Vue component testing with Vitest. Test props, rendering, and date formatting.</standards>
<locations>app/components/**/*.spec.ts</locations>
<ideas>
<idea ac="AC1">Mount ResumeHeader with sample data, verify name displays with correct styling</idea>
<idea ac="AC2">Verify job title displays below name</idea>
<idea ac="AC3">Check for blue border element</idea>
<idea ac="AC5">Mount ResumeSummary, verify paragraph renders</idea>
<idea ac="AC6">Check computed line-height is 1.6</idea>
<idea ac="AC7">Mount ResumeExperience with multiple jobs, verify all fields display</idea>
<idea ac="AC8">Test job sorting by date</idea>
<idea ac="AC9">Test with job missing endDate, verify "Present" displays</idea>
</ideas>
</tests>
</story-context>
@@ -0,0 +1,257 @@
# Story 2.3: Create Resume Header & Main Content Components
Status: ready-for-dev
## Story
As a user,
I want to see my name, title, summary, and work experience prominently,
so that recruiters see the most important information first.
## Acceptance Criteria
### ResumeHeader.vue
1. **AC1:** Given the header component renders, when it displays, then it shows full name in large bold text (2rem, font-bold)
2. **AC2:** Job title appears below name (1.25rem, text-gray-600)
3. **AC3:** Blue accent line or background element is present
### ResumeSummary.vue
4. **AC4:** Given the summary component renders, when it displays, then it shows "Profile" or "Summary" section header
5. **AC5:** Professional summary paragraph is displayed
6. **AC6:** Line height is 1.6 for readability
### ResumeExperience.vue
7. **AC7:** Given the experience component renders, when it displays, then it shows for each job: position title (bold), company name, date range (formatted: "Jan 2022 - Present"), bullet points for highlights (• character)
8. **AC8:** Jobs are sorted by date (most recent first)
9. **AC9:** "Present" is shown for current jobs (no endDate)
## Tasks / Subtasks
- [ ] Create ResumeHeader component (AC: #1-#3)
- [ ] Create `app/components/resume/ResumeHeader.vue`
- [ ] Accept props: `name` (string), `label` (string)
- [ ] Display name with `text-3xl font-bold` (2rem)
- [ ] Display job title with `text-xl text-gray-600` (1.25rem)
- [ ] Add blue accent line: `border-b-4 border-blue-600`
- [ ] Create ResumeSummary component (AC: #4-#6)
- [ ] Create `app/components/resume/ResumeSummary.vue`
- [ ] Accept props: `summary` (string)
- [ ] Add section header "Profile" with `text-base font-semibold`
- [ ] Display summary paragraph with `leading-relaxed` (line-height: 1.6)
- [ ] Use `text-sm text-gray-800`
- [ ] Create ResumeExperience component (AC: #7-#9)
- [ ] Create `app/components/resume/ResumeExperience.vue`
- [ ] Accept props: `work` (WorkExperience[])
- [ ] Add section header "Experience" with `text-base font-semibold`
- [ ] Sort jobs by startDate (most recent first)
- [ ] For each job, display:
- Position title: `font-semibold text-gray-900`
- Company name: `text-gray-700`
- Date range: Use `formatDate()` helper from composable
- Highlights: `<ul>` with bullet points (• character)
- [ ] Handle current jobs: Show "Present" if no endDate
- [ ] Integrate components into ResumePreview (AC: #1-#9)
- [ ] Import all three components in `ResumePreview.vue`
- [ ] Pass data from `useResumeData()` composable
- [ ] Place in right main content column:
- ResumeHeader at top
- ResumeSummary below header
- ResumeExperience below summary
- [ ] Test components rendering
- [ ] Verify header displays name and title correctly
- [ ] Check blue accent line visibility
- [ ] Verify summary section with proper line height
- [ ] Test experience section with multiple jobs
- [ ] Verify date formatting ("Jan 2022 - Present")
- [ ] Check job sorting (most recent first)
- [ ] Test with current job (no endDate)
## Dev Notes
### Architecture Alignment
**From Architecture Doc:**
- File locations: `app/components/resume/ResumeHeader.vue`, `ResumeSummary.vue`, `ResumeExperience.vue`
- Data source: Props passed from `ResumePreview.vue` (which uses `useResumeData()`)
- Date formatting: Use `formatDate()` helper from composable
**From Tech Spec Epic 2:**
- AC5, AC10, AC11, AC15, AC20, AC21 map to this story
- Main content components for right column (65% width)
- Professional typography and spacing
### Learnings from Previous Story
**From Story 2.2 (Status: drafted)**
- `ResumePreview.vue` container created with two-column grid
- Right main content column ready for child components
- Data access via `useResumeData()` composable established
- Tailwind styling patterns defined (text-sm, text-blue-600, etc.)
- **Integration Point**: Import these components into right column of ResumePreview
[Source: docs/sprint-artifacts/2-2-create-resume-preview-container-component.md]
### Project Structure Notes
**Files to Create:**
- `app/components/resume/ResumeHeader.vue`
- `app/components/resume/ResumeSummary.vue`
- `app/components/resume/ResumeExperience.vue`
**Files to Modify:**
- `app/components/resume/ResumePreview.vue` - Import and integrate new components
**Dependencies:**
- `app/composables/useResumeData.ts` - `formatDate()` helper
- `app/types/resume.ts` - `WorkExperience` interface
- `app/data/resume.en.ts` - Sample data
### Implementation Notes
**ResumeHeader.vue:**
```vue
<script setup lang="ts">
interface Props {
name: string
label: string
}
defineProps<Props>()
</script>
<template>
<div class="border-b-4 border-blue-600 pb-4 mb-6">
<h1 class="text-3xl font-bold text-gray-900">{{ name }}</h1>
<p class="text-xl text-gray-600 mt-1">{{ label }}</p>
</div>
</template>
```
**ResumeSummary.vue:**
```vue
<script setup lang="ts">
interface Props {
summary: string
}
defineProps<Props>()
</script>
<template>
<div class="mb-6">
<h2 class="text-base font-semibold text-gray-900 mb-2">Profile</h2>
<p class="text-sm text-gray-800 leading-relaxed">{{ summary }}</p>
</div>
</template>
```
**ResumeExperience.vue:**
```vue
<script setup lang="ts">
import type { WorkExperience } from '~/types/resume'
interface Props {
work: WorkExperience[]
}
const props = defineProps<Props>()
const { formatDate } = useResumeData()
const sortedWork = computed(() => {
return [...props.work].sort((a, b) => {
const dateA = a.startDate || ''
const dateB = b.startDate || ''
return dateB.localeCompare(dateA) // Most recent first
})
})
const formatDateRange = (start: string, end?: string) => {
const startFormatted = formatDate(start)
const endFormatted = end ? formatDate(end) : 'Present'
return `${startFormatted} - ${endFormatted}`
}
</script>
<template>
<div>
<h2 class="text-base font-semibold text-gray-900 mb-4">Experience</h2>
<div v-for="job in sortedWork" :key="job.company + job.position" class="mb-6">
<h3 class="font-semibold text-gray-900">{{ job.position }}</h3>
<p class="text-sm text-gray-700">{{ job.company }}</p>
<p class="text-xs text-gray-600 mb-2">{{ formatDateRange(job.startDate, job.endDate) }}</p>
<ul class="text-sm text-gray-800 space-y-1">
<li v-for="(highlight, idx) in job.highlights" :key="idx" class="flex">
<span class="mr-2"></span>
<span>{{ highlight }}</span>
</li>
</ul>
</div>
</div>
</template>
```
**Integration in ResumePreview.vue:**
```vue
<!-- Right Main Content -->
<div class="p-6 space-y-6">
<ResumeHeader
:name="resume.basics.name"
:label="resume.basics.label"
/>
<ResumeSummary :summary="resume.basics.summary" />
<ResumeExperience :work="resume.work" />
</div>
```
**Testing Checklist:**
- [ ] Header displays name "Ali Arghyani" in large bold text
- [ ] Job title displays below name in gray
- [ ] Blue accent line visible under header
- [ ] Summary section has "Profile" header
- [ ] Summary paragraph has proper line height (1.6)
- [ ] Experience section has "Experience" header
- [ ] Jobs sorted by date (most recent first)
- [ ] Each job shows position, company, date range, highlights
- [ ] Date formatting works: "Jan 2022 - Present"
- [ ] Bullet points (•) display correctly
- [ ] Current job shows "Present" instead of end date
### References
- [Source: docs/architecture.md#Data-Architecture]
- [Source: docs/architecture.md#Consistency-Rules]
- [Source: docs/sprint-artifacts/tech-spec-epic-2.md#AC5-Header-Section]
- [Source: docs/sprint-artifacts/tech-spec-epic-2.md#AC10-Summary-Section]
- [Source: docs/sprint-artifacts/tech-spec-epic-2.md#AC11-Experience-Section]
- [Source: docs/epics.md#Story-2.3-Create-Resume-Header-Main-Content-Components]
## Dev Agent Record
### Context Reference
- docs/sprint-artifacts/2-3-create-resume-header-main-content-components.context.xml
### Agent Model Used
<!-- Will be filled by dev agent -->
### Debug Log References
<!-- Will be filled by dev agent during implementation -->
### Completion Notes List
<!-- Will be filled by dev agent after completion -->
### File List
<!-- Will be filled by dev agent with created/modified files -->
---
**Change Log:**
- 2025-11-30: Story drafted by SM agent (mahdi)
@@ -0,0 +1,140 @@
<story-context id="2-4-create-resume-sidebar-components" v="1.0">
<metadata>
<epicId>2</epicId>
<storyId>2.4</storyId>
<title>Create Resume Sidebar Components</title>
<status>ready-for-dev</status>
<generatedAt>2025-11-30</generatedAt>
<generator>BMAD Story Context Workflow</generator>
<sourceStoryPath>docs/sprint-artifacts/2-4-create-resume-sidebar-components.md</sourceStoryPath>
</metadata>
<story>
<asA>user</asA>
<iWant>to see my contact info, skills, education, and languages in the sidebar</iWant>
<soThat>this supporting information is easily scannable</soThat>
<tasks>
- Create ResumeContact component
- Create ResumeSkills component
- Create ResumeEducation component
- Create ResumeLanguages component
- Integrate components into ResumePreview
- Test components rendering
</tasks>
</story>
<acceptanceCriteria>
<criterion id="AC1">Contact shows email with icon (i-mdi-email)</criterion>
<criterion id="AC2">Phone with icon (i-mdi-phone)</criterion>
<criterion id="AC3">Location with icon (i-mdi-map-marker)</criterion>
<criterion id="AC4">Social profiles with icons (LinkedIn, GitHub, etc.)</criterion>
<criterion id="AC5">Each item is on its own line</criterion>
<criterion id="AC6">Links are clickable (mailto:, tel:, https://)</criterion>
<criterion id="AC7">Skills shows "Skills" section header</criterion>
<criterion id="AC8">Skill categories as subheaders</criterion>
<criterion id="AC9">Keywords as tags or comma-separated list</criterion>
<criterion id="AC10">Education shows degree type and field (e.g., "B.Sc. Computer Science")</criterion>
<criterion id="AC11">Institution name</criterion>
<criterion id="AC12">Date range</criterion>
<criterion id="AC13">Languages shows "Languages" section header</criterion>
<criterion id="AC14">Each language with fluency level</criterion>
</acceptanceCriteria>
<artifacts>
<docs>
<doc>
<path>docs/architecture.md</path>
<title>Resume Export Feature - Architecture Document</title>
<section>Data Architecture</section>
<snippet>ResumeBasics: email, phone, location {city, country}, profiles [{network, url, icon}]. Skill: {name, keywords[]}. Education: {institution, area, studyType, startDate, endDate}. Language: {language, fluency}.</snippet>
</doc>
<doc>
<path>docs/sprint-artifacts/tech-spec-epic-2.md</path>
<title>Epic Technical Specification: Resume Preview Page</title>
<section>AC6-AC9: Sidebar Components</section>
<snippet>Contact: Icons with clickable links. Skills: Categories with keywords. Education: Degree, institution, dates. Languages: Language with fluency.</snippet>
</doc>
</docs>
<code>
<artifact>
<path>app/components/resume/ResumePreview.vue</path>
<kind>component</kind>
<symbol>ResumePreview</symbol>
<reason>Parent container where sidebar components will be integrated</reason>
</artifact>
<artifact>
<path>app/composables/useResumeData.ts</path>
<kind>composable</kind>
<symbol>useResumeData, formatDate</symbol>
<reason>Provides data and date formatting</reason>
</artifact>
<artifact>
<path>app/types/resume.ts</path>
<kind>types</kind>
<symbol>ResumeBasics, Skill, Education, Language</symbol>
<reason>TypeScript interfaces for props</reason>
</artifact>
</code>
<dependencies>
<node>
<package name="@nuxt/ui" version="^4.0.x" />
</node>
</dependencies>
</artifacts>
<constraints>
- Use Nuxt UI UIcon component for all icons
- Icon names: i-mdi-email, i-mdi-phone, i-mdi-map-marker, i-mdi-linkedin, i-mdi-github
- Links: mailto: for email, tel: for phone, https:// for social profiles
- Section headers: text-base font-semibold text-gray-900 mb-4
- Body text: text-sm for content, text-xs for secondary info
- File locations: app/components/resume/ResumeContact.vue, ResumeSkills.vue, ResumeEducation.vue, ResumeLanguages.vue
- Skills keywords: Join with ', ' (comma-separated)
- Education format: "{studyType} {area}"
- Languages format: "{language} - {fluency}"
</constraints>
<interfaces>
<interface>
<name>ResumeContact Props</name>
<kind>Vue component props</kind>
<signature>interface Props { basics: ResumeBasics }</signature>
<path>app/components/resume/ResumeContact.vue</path>
</interface>
<interface>
<name>ResumeSkills Props</name>
<kind>Vue component props</kind>
<signature>interface Props { skills: Skill[] }</signature>
<path>app/components/resume/ResumeSkills.vue</path>
</interface>
<interface>
<name>ResumeEducation Props</name>
<kind>Vue component props</kind>
<signature>interface Props { education: Education[] }</signature>
<path>app/components/resume/ResumeEducation.vue</path>
</interface>
<interface>
<name>ResumeLanguages Props</name>
<kind>Vue component props</kind>
<signature>interface Props { languages?: Language[] }</signature>
<path>app/components/resume/ResumeLanguages.vue</path>
</interface>
<interface>
<name>UIcon</name>
<kind>Nuxt UI component</kind>
<signature>&lt;UIcon name="icon-name" class="..." /&gt;</signature>
<path>@nuxt/ui</path>
</interface>
</interfaces>
<tests>
<standards>Vue component testing with Vitest. Test props, icon rendering, and link functionality.</standards>
<locations>app/components/**/*.spec.ts</locations>
<ideas>
<idea ac="AC1-AC6">Mount ResumeContact, verify all contact fields and icons display, test link hrefs</idea>
<idea ac="AC7-AC9">Mount ResumeSkills, verify categories and keywords display correctly</idea>
<idea ac="AC10-AC12">Mount ResumeEducation, verify degree format and date range</idea>
<idea ac="AC13-AC14">Mount ResumeLanguages, verify language list with fluency levels</idea>
</ideas>
</tests>
</story-context>
@@ -0,0 +1,324 @@
# Story 2.4: Create Resume Sidebar Components
Status: ready-for-dev
## Story
As a user,
I want to see my contact info, skills, education, and languages in the sidebar,
so that this supporting information is easily scannable.
## Acceptance Criteria
### ResumeContact.vue
1. **AC1:** Given the contact component renders, when it displays, then it shows email with icon (i-mdi-email)
2. **AC2:** Phone with icon (i-mdi-phone)
3. **AC3:** Location with icon (i-mdi-map-marker)
4. **AC4:** Social profiles with icons (LinkedIn, GitHub, etc.)
5. **AC5:** Each item is on its own line
6. **AC6:** Links are clickable (mailto:, tel:, https://)
### ResumeSkills.vue
7. **AC7:** Given the skills component renders, when it displays, then it shows "Skills" section header
8. **AC8:** Skill categories as subheaders
9. **AC9:** Keywords as tags or comma-separated list
### ResumeEducation.vue
10. **AC10:** Given the education component renders, when it displays, then it shows degree type and field (e.g., "B.Sc. Computer Science")
11. **AC11:** Institution name
12. **AC12:** Date range
### ResumeLanguages.vue
13. **AC13:** Given the languages component renders, when it displays, then it shows "Languages" section header
14. **AC14:** Each language with fluency level
## Tasks / Subtasks
- [ ] Create ResumeContact component (AC: #1-#6)
- [ ] Create `app/components/resume/ResumeContact.vue`
- [ ] Accept props: `basics` (ResumeBasics)
- [ ] Display email with icon and mailto: link
- [ ] Display phone with icon and tel: link
- [ ] Display location with icon (city, country)
- [ ] Display social profiles with icons and https:// links
- [ ] Use Nuxt UI `UIcon` component for icons
- [ ] Style each item on separate line
- [ ] Create ResumeSkills component (AC: #7-#9)
- [ ] Create `app/components/resume/ResumeSkills.vue`
- [ ] Accept props: `skills` (Skill[])
- [ ] Add section header "Skills"
- [ ] Display each skill category as subheader
- [ ] Display keywords as comma-separated list or tags
- [ ] Use consistent styling with other sidebar sections
- [ ] Create ResumeEducation component (AC: #10-#12)
- [ ] Create `app/components/resume/ResumeEducation.vue`
- [ ] Accept props: `education` (Education[])
- [ ] Add section header "Education"
- [ ] Display degree type and field (e.g., "B.Sc. Computer Science")
- [ ] Display institution name
- [ ] Display date range using `formatDate()` helper
- [ ] Create ResumeLanguages component (AC: #13-#14)
- [ ] Create `app/components/resume/ResumeLanguages.vue`
- [ ] Accept props: `languages` (Language[])
- [ ] Add section header "Languages"
- [ ] Display each language with fluency level
- [ ] Format: "English - Fluent"
- [ ] Integrate components into ResumePreview (AC: #1-#14)
- [ ] Import all four components in `ResumePreview.vue`
- [ ] Pass data from `useResumeData()` composable
- [ ] Place in left sidebar column:
- ResumeContact at top
- ResumeSkills below contact
- ResumeEducation below skills
- ResumeLanguages at bottom
- [ ] Test components rendering
- [ ] Verify contact info displays with icons
- [ ] Test clickable links (email, phone, social)
- [ ] Check skills section with categories and keywords
- [ ] Verify education section with degree and dates
- [ ] Test languages section with fluency levels
- [ ] Verify all components fit in sidebar layout
## Dev Notes
### Architecture Alignment
**From Architecture Doc:**
- File locations: `app/components/resume/ResumeContact.vue`, `ResumeSkills.vue`, `ResumeEducation.vue`, `ResumeLanguages.vue`
- Icons: Use Nuxt UI `UIcon` component with Iconify icons
- Data source: Props passed from `ResumePreview.vue`
**From Tech Spec Epic 2:**
- AC6, AC7, AC8, AC9, AC16, AC17, AC18, AC19 map to this story
- Sidebar components for left column (35% width)
- Consistent section header styling
### Learnings from Previous Story
**From Story 2.3 (Status: drafted)**
- Main content components created (Header, Summary, Experience)
- Component prop patterns established
- Section header styling: `text-base font-semibold text-gray-900 mb-2`
- Date formatting via `formatDate()` helper
- **Integration Point**: Import sidebar components into left column of ResumePreview
[Source: docs/sprint-artifacts/2-3-create-resume-header-main-content-components.md]
### Project Structure Notes
**Files to Create:**
- `app/components/resume/ResumeContact.vue`
- `app/components/resume/ResumeSkills.vue`
- `app/components/resume/ResumeEducation.vue`
- `app/components/resume/ResumeLanguages.vue`
**Files to Modify:**
- `app/components/resume/ResumePreview.vue` - Import and integrate sidebar components
**Dependencies:**
- `@nuxt/ui` - `UIcon` component
- `app/composables/useResumeData.ts` - `formatDate()` helper
- `app/types/resume.ts` - Type interfaces
- `app/data/resume.en.ts` - Sample data
### Implementation Notes
**ResumeContact.vue:**
```vue
<script setup lang="ts">
import type { ResumeBasics } from '~/types/resume'
interface Props {
basics: ResumeBasics
}
defineProps<Props>()
</script>
<template>
<div class="space-y-3">
<h2 class="text-base font-semibold text-gray-900 mb-4">Contact</h2>
<!-- Email -->
<div class="flex items-center gap-2 text-sm">
<UIcon name="i-mdi-email" class="text-blue-600 flex-shrink-0" />
<a :href="`mailto:${basics.email}`" class="text-gray-700 hover:text-blue-600">
{{ basics.email }}
</a>
</div>
<!-- Phone -->
<div class="flex items-center gap-2 text-sm">
<UIcon name="i-mdi-phone" class="text-blue-600 flex-shrink-0" />
<a :href="`tel:${basics.phone}`" class="text-gray-700 hover:text-blue-600">
{{ basics.phone }}
</a>
</div>
<!-- Location -->
<div class="flex items-center gap-2 text-sm">
<UIcon name="i-mdi-map-marker" class="text-blue-600 flex-shrink-0" />
<span class="text-gray-700">
{{ basics.location.city }}, {{ basics.location.country }}
</span>
</div>
<!-- Social Profiles -->
<div v-for="profile in basics.profiles" :key="profile.network" class="flex items-center gap-2 text-sm">
<UIcon :name="profile.icon || 'i-mdi-link'" class="text-blue-600 flex-shrink-0" />
<a :href="profile.url" target="_blank" class="text-gray-700 hover:text-blue-600">
{{ profile.network }}
</a>
</div>
</div>
</template>
```
**ResumeSkills.vue:**
```vue
<script setup lang="ts">
import type { Skill } from '~/types/resume'
interface Props {
skills: Skill[]
}
defineProps<Props>()
</script>
<template>
<div class="space-y-4">
<h2 class="text-base font-semibold text-gray-900 mb-4">Skills</h2>
<div v-for="skill in skills" :key="skill.name" class="space-y-1">
<h3 class="text-sm font-medium text-gray-800">{{ skill.name }}</h3>
<p class="text-xs text-gray-600">
{{ skill.keywords.join(', ') }}
</p>
</div>
</div>
</template>
```
**ResumeEducation.vue:**
```vue
<script setup lang="ts">
import type { Education } from '~/types/resume'
interface Props {
education: Education[]
}
defineProps<Props>()
const { formatDate } = useResumeData()
const formatDateRange = (start: string, end?: string) => {
const startFormatted = formatDate(start)
const endFormatted = end ? formatDate(end) : 'Present'
return `${startFormatted} - ${endFormatted}`
}
</script>
<template>
<div class="space-y-4">
<h2 class="text-base font-semibold text-gray-900 mb-4">Education</h2>
<div v-for="edu in education" :key="edu.institution" class="space-y-1">
<h3 class="text-sm font-medium text-gray-800">
{{ edu.studyType }} {{ edu.area }}
</h3>
<p class="text-xs text-gray-600">{{ edu.institution }}</p>
<p class="text-xs text-gray-500">{{ formatDateRange(edu.startDate, edu.endDate) }}</p>
</div>
</div>
</template>
```
**ResumeLanguages.vue:**
```vue
<script setup lang="ts">
import type { Language } from '~/types/resume'
interface Props {
languages?: Language[]
}
defineProps<Props>()
</script>
<template>
<div v-if="languages && languages.length > 0" class="space-y-2">
<h2 class="text-base font-semibold text-gray-900 mb-4">Languages</h2>
<div v-for="lang in languages" :key="lang.language" class="text-sm text-gray-700">
{{ lang.language }} - {{ lang.fluency }}
</div>
</div>
</template>
```
**Integration in ResumePreview.vue:**
```vue
<!-- Left Sidebar -->
<div class="bg-blue-50 p-6 space-y-6">
<ResumeContact :basics="resume.basics" />
<ResumeSkills :skills="resume.skills" />
<ResumeEducation :education="resume.education" />
<ResumeLanguages :languages="resume.languages" />
</div>
```
**Testing Checklist:**
- [ ] Contact section displays email, phone, location with icons
- [ ] Email link opens mailto: client
- [ ] Phone link opens tel: dialer
- [ ] Social profile links open in new tab
- [ ] Skills section shows categories and keywords
- [ ] Keywords displayed as comma-separated list
- [ ] Education section shows degree, institution, dates
- [ ] Date formatting works correctly
- [ ] Languages section shows language and fluency
- [ ] All components fit properly in sidebar
- [ ] Icons render correctly (Nuxt UI Icon)
### References
- [Source: docs/architecture.md#Data-Architecture]
- [Source: docs/architecture.md#Consistency-Rules]
- [Source: docs/sprint-artifacts/tech-spec-epic-2.md#AC6-Contact-Section]
- [Source: docs/sprint-artifacts/tech-spec-epic-2.md#AC7-Skills-Section]
- [Source: docs/sprint-artifacts/tech-spec-epic-2.md#AC8-Education-Section]
- [Source: docs/sprint-artifacts/tech-spec-epic-2.md#AC9-Languages-Section]
- [Source: docs/epics.md#Story-2.4-Create-Resume-Sidebar-Components]
## Dev Agent Record
### Context Reference
- docs/sprint-artifacts/2-4-create-resume-sidebar-components.context.xml
### Agent Model Used
<!-- Will be filled by dev agent -->
### Debug Log References
<!-- Will be filled by dev agent during implementation -->
### Completion Notes List
<!-- Will be filled by dev agent after completion -->
### File List
<!-- Will be filled by dev agent with created/modified files -->
---
**Change Log:**
- 2025-11-30: Story drafted by SM agent (mahdi)
@@ -0,0 +1,108 @@
<story-context id="2-5-create-download-button-component" v="1.0">
<metadata>
<epicId>2</epicId>
<storyId>2.5</storyId>
<title>Create Download Button Component</title>
<status>ready-for-dev</status>
<generatedAt>2025-11-30</generatedAt>
<generator>BMAD Story Context Workflow</generator>
<sourceStoryPath>docs/sprint-artifacts/2-5-create-download-button-component.md</sourceStoryPath>
</metadata>
<story>
<asA>user</asA>
<iWant>a prominent download button</iWant>
<soThat>I can easily download my resume as PDF</soThat>
<tasks>
- Create ResumeDownloadButton component
- Implement print mode detection
- Add placeholder click handler
- Integrate into resume page
- Test button functionality
</tasks>
</story>
<acceptanceCriteria>
<criterion id="AC1">Button is a floating action button (FAB) in bottom-right corner</criterion>
<criterion id="AC2">Has download icon (i-heroicons-arrow-down-tray)</criterion>
<criterion id="AC3">Has "Download PDF" text (or just icon on mobile)</criterion>
<criterion id="AC4">Has blue background color</criterion>
<criterion id="AC5">Has fixed position (doesn't scroll)</criterion>
<criterion id="AC6">Has shadow for elevation</criterion>
<criterion id="AC7">Has the `.no-print` class (hidden in PDF)</criterion>
<criterion id="AC8">Button is not visible when `?print=true` parameter is present</criterion>
</acceptanceCriteria>
<artifacts>
<docs>
<doc>
<path>docs/architecture.md</path>
<title>Resume Export Feature - Architecture Document</title>
<section>Novel Pattern: WYSIWYG PDF Export</section>
<snippet>Query Parameter: `?print=true` hides download button for PDF generation.</snippet>
</doc>
<doc>
<path>docs/sprint-artifacts/tech-spec-epic-2.md</path>
<title>Epic Technical Specification: Resume Preview Page</title>
<section>AC12: Download Button</section>
<snippet>Floating action button in bottom-right corner with download icon, blue background, fixed position, shadow, and .no-print class.</snippet>
</doc>
</docs>
<code>
<artifact>
<path>app/pages/resume.vue</path>
<kind>page</kind>
<symbol>resume page</symbol>
<reason>Will import and render this button component, provides isPrintMode prop</reason>
</artifact>
</code>
<dependencies>
<node>
<package name="@nuxt/ui" version="^4.0.x" />
</node>
</dependencies>
</artifacts>
<constraints>
- Use Nuxt UI UButton component
- Icon: i-heroicons-arrow-down-tray
- Position: fixed bottom-6 right-6
- Color: primary (blue)
- Size: lg
- Shadow: shadow-lg
- Z-index: z-50 (above all content)
- Responsive: Hide text on mobile with "hidden sm:inline"
- Print: .no-print class and v-if="!isPrintMode"
- File location: app/components/resume/ResumeDownloadButton.vue
- Click handler: Placeholder console.log (Epic 3 will implement actual PDF download)
</constraints>
<interfaces>
<interface>
<name>ResumeDownloadButton Props</name>
<kind>Vue component props</kind>
<signature>interface Props { isPrintMode?: boolean }</signature>
<path>app/components/resume/ResumeDownloadButton.vue</path>
</interface>
<interface>
<name>UButton</name>
<kind>Nuxt UI component</kind>
<signature>&lt;UButton icon="..." size="..." color="..." class="..." @click="..." /&gt;</signature>
<path>@nuxt/ui</path>
</interface>
</interfaces>
<tests>
<standards>Vue component testing with Vitest. Test visibility, positioning, and click handler.</standards>
<locations>app/components/**/*.spec.ts</locations>
<ideas>
<idea ac="AC1">Mount component, verify fixed positioning and bottom-right placement</idea>
<idea ac="AC2">Check icon prop is set correctly</idea>
<idea ac="AC4">Verify blue background color</idea>
<idea ac="AC5">Check fixed position class</idea>
<idea ac="AC6">Verify shadow class applied</idea>
<idea ac="AC7">Check .no-print class exists</idea>
<idea ac="AC8">Mount with isPrintMode=true, verify button not rendered</idea>
</ideas>
</tests>
</story-context>
@@ -0,0 +1,227 @@
# Story 2.5: Create Download Button Component
Status: ready-for-dev
## Story
As a user,
I want a prominent download button,
so that I can easily download my resume as PDF.
## Acceptance Criteria
1. **AC1:** Given I'm on the resume page, when I see the download button, then it's a floating action button (FAB) in bottom-right corner
2. **AC2:** It has download icon (i-heroicons-arrow-down-tray)
3. **AC3:** It has "Download PDF" text (or just icon on mobile)
4. **AC4:** It has blue background color
5. **AC5:** It has fixed position (doesn't scroll)
6. **AC6:** It has shadow for elevation
7. **AC7:** It has the `.no-print` class (hidden in PDF)
8. **AC8:** Given I'm on the page with `?print=true`, when the page renders, then the button is not visible
## Tasks / Subtasks
- [ ] Create ResumeDownloadButton component (AC: #1-#7)
- [ ] Create `app/components/resume/ResumeDownloadButton.vue`
- [ ] Use Nuxt UI `UButton` component
- [ ] Set icon to `i-heroicons-arrow-down-tray`
- [ ] Add "Download PDF" text
- [ ] Apply blue background: `color="primary"` or `bg-blue-600`
- [ ] Set fixed position: `fixed bottom-6 right-6`
- [ ] Add shadow: `shadow-lg`
- [ ] Add `.no-print` class
- [ ] Implement print mode detection (AC: #8)
- [ ] Accept prop: `isPrintMode` (boolean)
- [ ] Use `v-if="!isPrintMode"` to conditionally render
- [ ] Ensure button hidden when `?print=true`
- [ ] Add placeholder click handler
- [ ] Add `@click` event handler
- [ ] For now, log to console: "Download PDF clicked"
- [ ] Note: Actual PDF generation will be implemented in Epic 3
- [ ] Integrate into resume page (AC: #1-#8)
- [ ] Import component in `pages/resume.vue`
- [ ] Pass `isPrintMode` prop from route query
- [ ] Place button outside ResumePreview container
- [ ] Verify button appears in bottom-right corner
- [ ] Test button functionality
- [ ] Verify button appears in bottom-right corner
- [ ] Check fixed position (doesn't scroll with page)
- [ ] Verify blue background and shadow
- [ ] Test icon displays correctly
- [ ] Check "Download PDF" text visibility
- [ ] Test click handler (console log)
- [ ] Verify button hidden with `?print=true`
- [ ] Test print preview (Ctrl+P) - button should be hidden
## Dev Notes
### Architecture Alignment
**From Architecture Doc:**
- File location: `app/components/resume/ResumeDownloadButton.vue`
- Component: Use Nuxt UI `UButton` component
- Icon: Heroicons arrow-down-tray
- Position: Fixed bottom-right (FAB pattern)
**From Tech Spec Epic 2:**
- AC12 maps to this story
- Button hidden in print mode for PDF generation
- Placeholder for Epic 3 integration
### Learnings from Previous Story
**From Story 2.4 (Status: drafted)**
- Sidebar components created (Contact, Skills, Education, Languages)
- All resume sections now complete
- ResumePreview fully populated with content
- **Integration Point**: Add download button to resume page, outside ResumePreview container
**From Story 2.1 (Status: drafted)**
- Print mode detection implemented via `useRoute().query.print`
- `isPrintMode` computed property available in `pages/resume.vue`
- **Integration Point**: Pass `isPrintMode` to download button component
[Source: docs/sprint-artifacts/2-4-create-resume-sidebar-components.md]
[Source: docs/sprint-artifacts/2-1-create-resume-page-route.md]
### Project Structure Notes
**File to Create:**
- `app/components/resume/ResumeDownloadButton.vue`
**Files to Modify:**
- `app/pages/resume.vue` - Import and place download button
**Dependencies:**
- `@nuxt/ui` - `UButton` component
- Heroicons icon set (via Nuxt UI)
**Future Integration:**
- Epic 3 Story 3.2 will create `useResumePdf()` composable
- Epic 3 Story 3.3 will connect this button to PDF generation
### Implementation Notes
**ResumeDownloadButton.vue:**
```vue
<script setup lang="ts">
interface Props {
isPrintMode?: boolean
}
defineProps<Props>()
const handleDownload = () => {
// Placeholder - will be replaced in Epic 3 Story 3.3
console.log('Download PDF clicked')
// Future: const { downloadPdf } = useResumePdf()
// Future: await downloadPdf()
}
</script>
<template>
<UButton
v-if="!isPrintMode"
icon="i-heroicons-arrow-down-tray"
size="lg"
color="primary"
class="fixed bottom-6 right-6 shadow-lg no-print z-50"
@click="handleDownload"
>
<span class="hidden sm:inline">Download PDF</span>
</UButton>
</template>
<style scoped>
@media print {
.no-print {
display: none !important;
}
}
</style>
```
**Integration in pages/resume.vue:**
```vue
<script setup lang="ts">
definePageMeta({
layout: false
})
const route = useRoute()
const isPrintMode = computed(() => route.query.print === 'true')
useHead({
title: 'Resume - Ali Arghyani',
meta: [
{ name: 'robots', content: 'noindex' }
]
})
</script>
<template>
<div class="min-h-screen bg-white">
<ResumePreview />
<ResumeDownloadButton :is-print-mode="isPrintMode" />
</div>
</template>
```
**Responsive Behavior:**
- Desktop: Shows "Download PDF" text with icon
- Mobile: Shows icon only (text hidden with `hidden sm:inline`)
**Z-Index:**
- Set `z-50` to ensure button appears above all content
**Testing Checklist:**
- [ ] Button appears in bottom-right corner
- [ ] Button has fixed position (stays in place when scrolling)
- [ ] Blue background color applied
- [ ] Shadow visible for elevation
- [ ] Download icon displays correctly
- [ ] "Download PDF" text visible on desktop
- [ ] Text hidden on mobile (< 640px)
- [ ] Click handler logs to console
- [ ] Button hidden with `?print=true` parameter
- [ ] Button hidden in print preview (Ctrl+P)
- [ ] Button has proper z-index (appears above content)
### References
- [Source: docs/architecture.md#Implementation-Patterns]
- [Source: docs/architecture.md#Novel-Pattern-WYSIWYG-PDF-Export]
- [Source: docs/sprint-artifacts/tech-spec-epic-2.md#AC12-Download-Button]
- [Source: docs/sprint-artifacts/tech-spec-epic-2.md#AC13-Print-Mode]
- [Source: docs/epics.md#Story-2.5-Create-Download-Button-Component]
## Dev Agent Record
### Context Reference
- docs/sprint-artifacts/2-5-create-download-button-component.context.xml
### Agent Model Used
<!-- Will be filled by dev agent -->
### Debug Log References
<!-- Will be filled by dev agent during implementation -->
### Completion Notes List
<!-- Will be filled by dev agent after completion -->
### File List
<!-- Will be filled by dev agent with created/modified files -->
---
**Change Log:**
- 2025-11-30: Story drafted by SM agent (mahdi)
+6 -6
View File
@@ -47,12 +47,12 @@ development_status:
# Goal: Create pixel-perfect resume preview matching PDF output
# FRs: FR5-9, FR15-25
# ═══════════════════════════════════════════════════════════════
epic-2: backlog
2-1-create-resume-page-route: backlog
2-2-create-resume-preview-container-component: backlog
2-3-create-resume-header-main-content-components: backlog
2-4-create-resume-sidebar-components: backlog
2-5-create-download-button-component: backlog
epic-2: contexted
2-1-create-resume-page-route: ready-for-dev
2-2-create-resume-preview-container-component: ready-for-dev
2-3-create-resume-header-main-content-components: ready-for-dev
2-4-create-resume-sidebar-components: ready-for-dev
2-5-create-download-button-component: ready-for-dev
epic-2-retrospective: optional
# ═══════════════════════════════════════════════════════════════
+380
View File
@@ -0,0 +1,380 @@
# Epic Technical Specification: Resume Preview Page
Date: 2025-11-30
Author: mahdi
Epic ID: 2
Status: Draft
---
## Overview
Epic 2 delivers the Resume Preview Page - a pixel-perfect, standalone web page at `/resume` that renders the resume exactly as it will appear in the PDF export. This epic creates the visual foundation of the Resume Export feature, implementing all UI components with a two-column layout (sidebar + main content) using the Blue & White Clean Professional design template.
The preview page serves dual purposes: (1) user-facing preview for verification before download, and (2) source page for server-side PDF generation via Puppeteer. The WYSIWYG (What You See Is What You Get) approach ensures perfect consistency between web and PDF output.
## Objectives and Scope
**In Scope:**
- Standalone `/resume` route with no site navigation
- Two-column responsive layout (35% sidebar, 65% main content)
- Six modular Vue components for resume sections
- Blue (#2563eb) and white color scheme with Inter typography
- Print-optimized CSS for PDF generation compatibility
- A4 aspect ratio container (210mm × 297mm)
- Floating download button (hidden in print mode via `?print=true`)
- ATS-compatible HTML structure (semantic headings, no layout tables)
**Out of Scope:**
- PDF generation logic (Epic 3)
- Resume data creation (Epic 1 - already completed)
- Persian language support (future)
- Multiple template designs (future)
- Customization UI (future)
## System Architecture Alignment
**Framework:** Nuxt 4.1.3 with Vue 3 Composition API
**Styling:** Tailwind CSS 4.1.x with Nuxt UI 4.0.x components
**Fonts:** Inter (via @nuxt/fonts) for English text
**Icons:** Nuxt UI Icon component with Iconify icons
**Architecture Decisions Referenced:**
- ADR-002: Vue Components as Templates - Each resume section is a standalone component
- Novel Pattern: WYSIWYG PDF Export - Single component source for web and PDF
- Naming Conventions: PascalCase for components, Tailwind utilities for styling
**Data Source:** `useResumeData()` composable (from Epic 1) provides reactive access to `app/data/resume.en.ts`
## Detailed Design
### Services and Modules
| Component | Responsibility | Inputs | Outputs | Owner |
|-----------|---------------|--------|---------|-------|
| `pages/resume.vue` | Route handler, layout wrapper | Query param `?print` | Renders ResumePreview | Story 2.1 |
| `ResumePreview.vue` | Container, two-column grid | Resume data | Full resume layout | Story 2.2 |
| `ResumeHeader.vue` | Name and job title display | `basics.name`, `basics.label` | Header section | Story 2.3 |
| `ResumeSummary.vue` | Professional summary | `basics.summary` | Summary section | Story 2.3 |
| `ResumeExperience.vue` | Work history with highlights | `work[]` array | Experience section | Story 2.3 |
| `ResumeContact.vue` | Contact info with icons | `basics` (email, phone, location, profiles) | Contact section | Story 2.4 |
| `ResumeSkills.vue` | Technical skills by category | `skills[]` array | Skills section | Story 2.4 |
| `ResumeEducation.vue` | Education history | `education[]` array | Education section | Story 2.4 |
| `ResumeLanguages.vue` | Language proficiencies | `languages[]` array | Languages section | Story 2.4 |
| `ResumeDownloadButton.vue` | Floating action button | `?print` query param | Download trigger | Story 2.5 |
### Data Models and Contracts
**Input:** All components consume data from `useResumeData()` composable:
```typescript
const { resume, formatDate, getFullName, getPdfFilename } = useResumeData()
```
**Resume Data Structure (from Epic 1):**
- `resume.basics`: Name, label, email, phone, location, profiles, summary
- `resume.work[]`: Company, position, startDate, endDate, highlights[]
- `resume.education[]`: Institution, area, studyType, startDate, endDate
- `resume.skills[]`: Name (category), keywords[]
- `resume.languages[]`: Language, fluency
**Date Format:** YYYY-MM strings formatted to "Jan 2023" via `formatDate()` helper
### APIs and Interfaces
**Component Props:**
```typescript
// ResumePreview.vue - No props (uses composable)
// All child components receive data via props from parent
// ResumeHeader.vue
interface Props {
name: string
label: string // Job title
}
// ResumeExperience.vue
interface Props {
work: WorkExperience[]
}
// ResumeContact.vue
interface Props {
basics: ResumeBasics
}
// Similar pattern for other components
```
**Query Parameters:**
- `?print=true`: Hides download button, optimizes for PDF generation
### Workflows and Sequencing
**User Flow:**
1. User navigates to `/resume`
2. Page loads with `layout: false` (standalone)
3. `ResumePreview.vue` fetches data via `useResumeData()`
4. Two-column grid renders:
- Left sidebar: Contact → Skills → Education → Languages
- Right main: Header → Summary → Experience
5. Floating download button appears (bottom-right)
6. User clicks download → triggers Epic 3 PDF generation
**PDF Generation Flow (Epic 3 integration):**
1. Puppeteer navigates to `/resume?print=true`
2. Same components render without download button
3. Puppeteer captures page as PDF
4. Result: Pixel-perfect match to web preview
## Non-Functional Requirements
### Performance
- **Page Load:** < 1 second (static data, no API calls)
- **LCP (Largest Contentful Paint):** < 1.5s
- **CLS (Cumulative Layout Shift):** 0 (fixed dimensions)
- **Font Loading:** Inter font preloaded via @nuxt/fonts
**Strategy:** Inline critical CSS, use Tailwind JIT, no external API dependencies
### Security
- **No PII Exposure:** Resume data is static, no user input
- **SEO:** `noindex` meta tag for privacy (resume not indexed by search engines)
- **XSS Protection:** Vue's automatic escaping for all text content
### Reliability/Availability
- **Static Rendering:** Page works without JavaScript (SSR)
- **Graceful Degradation:** Print styles work even if JS fails
- **Error Handling:** Composable returns empty data if file missing (prevents crashes)
### Observability
- **Console Logging:** Development mode logs component mount/unmount
- **Error Boundaries:** Vue error handlers catch component failures
- **Performance Monitoring:** Nuxt DevTools tracks component render times
## Dependencies and Integrations
**External Dependencies:**
- `@nuxt/fonts` (0.11.x): Inter font loading
- `@nuxt/ui` (4.0.x): UButton, UIcon components
- Tailwind CSS (4.1.x): Utility classes
**Internal Dependencies:**
- `app/types/resume.ts`: TypeScript interfaces (Epic 1)
- `app/data/resume.en.ts`: Resume data (Epic 1)
- `app/composables/useResumeData.ts`: Data access composable (Epic 1)
**Integration Points:**
- Epic 3: `/api/resume/pdf` will navigate to `/resume?print=true`
- Future: Persian support will use `app/data/resume.fa.ts`
## Acceptance Criteria (Authoritative)
### AC1: Standalone Resume Route
**Given** I navigate to `/resume`
**When** the page loads
**Then** I see the resume preview with no site header/footer
**And** page title is "Resume - Ali Arghyani"
**And** meta tag `<meta name="robots" content="noindex">` is present
### AC2: Two-Column Layout
**Given** the resume page is rendered on desktop
**When** I view the layout
**Then** I see a two-column grid:
- Left sidebar (35% width): Contact, Skills, Education, Languages
- Right main content (65% width): Header, Summary, Experience
**And** container has A4 aspect ratio (210mm × 297mm)
**And** page margins are 24px (1.5rem)
### AC3: Responsive Behavior
**Given** the resume page is rendered on mobile
**When** viewport width < 768px
**Then** layout switches to single column
**And** sidebar sections appear above main content
### AC4: Color Scheme and Typography
**Given** the resume is displayed
**When** I inspect the styling
**Then** primary color is blue (#2563eb) for headers and icons
**And** background is white
**And** text is dark gray (#1f2937)
**And** font family is Inter
**And** body text size is 14px (0.875rem)
### AC5: Header Section
**Given** the header component renders
**When** I view the top of the resume
**Then** I see full name in large bold text (2rem)
**And** job title below name (1.25rem, gray)
**And** blue accent line or background element
### AC6: Contact Section
**Given** the contact component renders
**When** I view the sidebar
**Then** I see email with icon (i-mdi-email)
**And** phone with icon (i-mdi-phone)
**And** location with icon (i-mdi-map-marker)
**And** social profiles (LinkedIn, GitHub) with icons
**And** all links are clickable (mailto:, tel:, https://)
### AC7: Skills Section
**Given** the skills component renders
**When** I view the sidebar
**Then** I see "Skills" section header
**And** skill categories as subheaders
**And** keywords displayed as tags or comma-separated list
### AC8: Education Section
**Given** the education component renders
**When** I view the sidebar
**Then** I see degree type and field (e.g., "B.Sc. Computer Science")
**And** institution name
**And** date range formatted (e.g., "2018 - 2022")
### AC9: Languages Section
**Given** the languages component renders
**When** I view the sidebar
**Then** I see "Languages" section header
**And** each language with fluency level (e.g., "English - Fluent")
### AC10: Summary Section
**Given** the summary component renders
**When** I view the main content
**Then** I see "Profile" or "Summary" section header
**And** professional summary paragraph
**And** line height is 1.6 for readability
### AC11: Experience Section
**Given** the experience component renders
**When** I view the main content
**Then** I see for each job:
- Position title (bold)
- Company name
- Date range (formatted: "Jan 2022 - Present")
- Bullet points for highlights (• character)
**And** jobs are sorted by date (most recent first)
**And** "Present" is shown for current jobs (no endDate)
### AC12: Download Button
**Given** I'm on the resume page
**When** I view the page
**Then** I see a floating action button in bottom-right corner
**And** it has download icon (i-heroicons-arrow-down-tray)
**And** it has "Download PDF" text
**And** it has blue background color
**And** it has fixed position (doesn't scroll)
**And** it has shadow for elevation
**And** it has `.no-print` class
### AC13: Print Mode
**Given** I navigate to `/resume?print=true`
**When** the page loads
**Then** the download button is not visible
**And** all other content renders normally
### AC14: Print Styles
**Given** the page is printed or captured by Puppeteer
**When** print media query is active
**Then** `.no-print` elements are hidden
**And** colors print correctly (printBackground: true)
**And** page breaks are controlled
**And** A4 dimensions are maintained
### AC15: ATS Compatibility
**Given** the HTML structure is inspected
**When** I check semantic elements
**Then** name uses `<h1>` tag
**And** section headers use `<h2>` tags
**And** no layout tables are used (CSS Grid/Flexbox only)
**And** all text is real HTML text (not images)
## Traceability Mapping
| AC | Spec Section | Components | Test Idea |
|----|--------------|------------|-----------|
| AC1 | Standalone Route | `pages/resume.vue` | Navigate to /resume, verify no nav, check meta tags |
| AC2 | Two-Column Layout | `ResumePreview.vue` | Inspect grid, measure column widths, verify A4 ratio |
| AC3 | Responsive | `ResumePreview.vue` | Resize viewport, verify single column on mobile |
| AC4 | Color/Typography | All components | Inspect computed styles, verify colors and fonts |
| AC5 | Header | `ResumeHeader.vue` | Check h1 text, font size, job title styling |
| AC6 | Contact | `ResumeContact.vue` | Verify icons, links, click mailto/tel links |
| AC7 | Skills | `ResumeSkills.vue` | Check section header, categories, keywords |
| AC8 | Education | `ResumeEducation.vue` | Verify degree, institution, date formatting |
| AC9 | Languages | `ResumeLanguages.vue` | Check language list, fluency levels |
| AC10 | Summary | `ResumeSummary.vue` | Verify section header, paragraph, line height |
| AC11 | Experience | `ResumeExperience.vue` | Check job entries, date formatting, bullet points |
| AC12 | Download Button | `ResumeDownloadButton.vue` | Verify FAB position, icon, styling, shadow |
| AC13 | Print Mode | `pages/resume.vue` | Add ?print=true, verify button hidden |
| AC14 | Print Styles | CSS | Trigger print preview, verify styles |
| AC15 | ATS Compatibility | HTML structure | Inspect DOM, verify semantic tags, no tables |
## Risks, Assumptions, Open Questions
**Risks:**
- **Risk:** Font loading delay causes layout shift
- **Mitigation:** Use @nuxt/fonts with preload, font-display: swap
- **Risk:** Print styles differ across browsers
- **Mitigation:** Test in Chrome, Firefox, Safari; use Puppeteer for PDF (consistent)
- **Risk:** Long content causes page overflow
- **Mitigation:** Test with max content length, add overflow handling
**Assumptions:**
- **Assumption:** Inter font is ATS-compatible
- **Validation:** Inter is a standard web font, widely supported
- **Assumption:** Two-column layout works for all content lengths
- **Validation:** Test with varying experience entries (2-5 jobs)
- **Assumption:** Blue (#2563eb) has sufficient contrast
- **Validation:** WCAG AA contrast ratio verified (4.5:1 minimum)
**Open Questions:**
- **Question:** Should mobile view show download button?
- **Answer:** Yes, but consider smaller size or icon-only
- **Question:** How to handle very long job titles or company names?
- **Answer:** Use text truncation with ellipsis, test with max lengths
- **Question:** Should we add a "Print" button in addition to "Download PDF"?
- **Answer:** No for MVP, browser print (Ctrl+P) is sufficient
## Test Strategy Summary
**Unit Tests:**
- Each component renders with sample data
- Date formatting helper works correctly
- Composable returns expected data structure
**Integration Tests:**
- Full page renders with all components
- Print mode hides download button
- Responsive layout switches at breakpoint
**Visual Regression Tests:**
- Screenshot comparison: web preview vs expected design
- Print preview matches web preview
- Mobile layout matches design
**Manual Tests:**
- Navigate to /resume, verify all sections
- Click all links (email, phone, social profiles)
- Test print preview (Ctrl+P)
- Test on mobile device
- Verify ATS compatibility (copy/paste text from PDF)
**Performance Tests:**
- Measure page load time (< 1s target)
- Check LCP and CLS metrics
- Verify font loading doesn't block render
**Acceptance Tests:**
- Run through all 15 ACs with real data
- Verify pixel-perfect match to design template
- Confirm Epic 3 can generate PDF from this page
---
**Epic 2 Tech Spec Complete**
**Status:** Ready for Story Creation
**Next Step:** SM creates Story 2.1 draft