--- sessionId: session-261002-153750-168j --- # Requirements ### Overview & Goals `HomeView.vue` currently acts as a monolithic view holding layout orchestration, proposal reading and paragraph rendering, amendment carousel presentation, comments placeholder, voting action buttons, export-to-markdown generation and file downloading, and empty/loading states. The goal is to componentize `HomeView.vue` into modular, single-responsibility Vue 3 components and shared utility functions. This reduces template verbosity, enhances maintainability and testability, eliminates duplicated markdown parsing across the project, and establishes clean component boundaries. ### Scope #### In Scope - Extracting standalone proposal components: - `ProposalDetail.vue`: Primary article layout combining proposal header, formatted paragraphs, amendments section, comments section, and the action rail. - `ProposalVotingCard.vue`: Voting options card (Approve, Reject, Abstain). - `ProposalExportCard.vue`: Markdown export card with download trigger. - `ProposalEmptyState.vue`: Informational empty state when no proposal is selected. - Extracting reusable utilities: - `src/utils/markdown.ts`: Centralized markdown parsing and DOM sanitization. - `src/utils/proposalExport.ts`: Proposal Markdown file generator and file download trigger. - Refactoring `src/views/HomeView.vue` to orchestrate layout, data fetching, and loading states cleanly. - Updating `ProposalContainer.vue` to use the shared markdown utility. #### Out of Scope - Backend API schema changes or new endpoints. - Redesigning the visual styles or changing Tailwind classes and responsive behavior. - Modifying `Sidebar.vue` or modal dialogs beyond their existing integration. ### User Stories - **As a developer**, I want `HomeView.vue` to be concise and focused on high-level layout so that I can easily navigate and maintain page-level logic. - **As a developer**, I want proposal voting, export, and presentation logic split into dedicated components so that they can be tested and modified independently without risking regressions across the entire page. - **As a user**, I want proposal display, voting, and file downloading to function seamlessly with identical UI and responsive interactions. ### Functional Requirements - **FR-1**: When no proposal is selected, the application displays `ProposalEmptyState` with the title "Select a proposal" and helper text. - **FR-2**: When a proposal is selected, `ProposalDetail` renders: - Proposal header with tags, creator wallet address, and proposal name. - Proposal paragraphs formatted using sanitized Markdown. - `AmendmentCarousel` with the current proposal ID. - Comments section placeholder. - Right-hand action rail containing `ProposalVotingCard` and `ProposalExportCard`. - **FR-3**: Clicking the download button in `ProposalExportCard` triggers the Markdown export and downloads a sanitized `.md` file containing proposal title, author, and paragraphs. - **FR-4**: Voting actions in `ProposalVotingCard` maintain their existing UI styling and emit selection events. - **FR-5**: Sidebar visibility toggles and loading spinners remain fully operational across desktop and mobile viewports. ### Non-Functional Requirements - **Maintainability**: Clear separation of presentation, business logic, and file utilities. - **Type Safety**: Full TypeScript typing for props, emits, and utility functions conforming to generated API models. - **Performance**: Zero overhead from component decomposition; no extra re-renders or layout shifts. # Technical Design ### Current Implementation `HomeView.vue` (245 lines) contains: - Primary container with responsive sidebar integration and mobile backdrop. - Proposal article layout with header, paragraph loops using inline `DOMPurify` / `marked` formatting, `AmendmentCarousel`, and comments container. - Aside rail containing voting buttons and the markdown download card. - Empty selection state. - Script containing proposal data fetching, markdown sanitization, file download generation, loading states, and sidebar toggling. - `ProposalContainer.vue` also duplicates markdown parsing logic (`marked` + `DOMPurify`). ### Key Decisions 1. **Component Hierarchy**: - `HomeView.vue` remains the top-level route coordinator: handles sidebar toggle state, fetching proposals list, and active proposal selection. - `ProposalDetail.vue` acts as the container for proposal inspection: receives `proposal: ProposalWithParagraphs` as a prop and embeds `AmendmentCarousel`, `ProposalVotingCard`, and `ProposalExportCard`. - `ProposalVotingCard.vue` and `ProposalExportCard.vue` are extracted as dedicated components to keep the voting workflow and export logic decoupled from reading layout. - `ProposalEmptyState.vue` handles the "no proposal selected" display. 2. **Utility Extraction (`src/utils/markdown.ts` & `src/utils/proposalExport.ts`)**: - Extract `applyFormatting` / `renderMarkdown` into `src/utils/markdown.ts` to prevent code duplication between `HomeView.vue` and `ProposalContainer.vue`. - Extract `downloadProposalMarkdown` and `sanitizeFilename` into `src/utils/proposalExport.ts` so file generation logic is pure, reusable, and easily testable. ### Components - **`src/views/HomeView.vue` (Modified)**: - Retains: `Sidebar`, mobile overlay, loading spinner, selection state, and window resize listeners. - Uses: `` and ``. - **`src/components/ProposalDetail.vue` (New)**: - Props: `proposal: ProposalWithParagraphs`. - Template: Proposal header, sanitized paragraph list, `AmendmentCarousel`, comments block, and sidebar rail embedding `ProposalVotingCard` and `ProposalExportCard`. - **`src/components/ProposalVotingCard.vue` (New)**: - Props: `proposalId: number`. - Emits: `vote: [type: 'Positive' | 'Negative' | 'Abstain']`. - Template: "Cast your vote" card with Approve, Reject, Abstain buttons. - **`src/components/ProposalExportCard.vue` (New)**: - Props: `proposalId: number`, `proposalName: string`. - Template: Proposal record card with `.md` badge and download action button. - **`src/components/ProposalEmptyState.vue` (New)**: - Template: Centered container with "Select a proposal" header and instructions. - **`src/components/ProposalContainer.vue` (Modified)**: - Refactored to import `renderMarkdown` from `src/utils/markdown.ts`. ### File Structure ``` src/ ├── components/ │ ├── AmendmentCarousel.vue (existing) │ ├── LoadingSpinner.vue (existing) │ ├── ProposalContainer.vue (modified - use markdown util) │ ├── ProposalDetail.vue (new) │ ├── ProposalEmptyState.vue (new) │ ├── ProposalExportCard.vue (new) │ ├── ProposalVotingCard.vue (new) │ └── Sidebar.vue (existing) ├── utils/ │ ├── markdown.ts (new) │ ├── proposalExport.ts (new) │ └── wallet.ts (existing) └── views/ └── HomeView.vue (modified - simplified coordinator) ``` ### Architecture Diagram ```mermaid graph TD HomeView["HomeView.vue (Coordinator)"] Sidebar["Sidebar.vue"] ProposalEmptyState["ProposalEmptyState.vue"] ProposalDetail["ProposalDetail.vue"] AmendmentCarousel["AmendmentCarousel.vue"] ProposalVotingCard["ProposalVotingCard.vue"] ProposalExportCard["ProposalExportCard.vue"] MarkdownUtil["utils/markdown.ts"] ExportUtil["utils/proposalExport.ts"] HomeView --> Sidebar HomeView --> ProposalEmptyState HomeView --> ProposalDetail ProposalDetail --> AmendmentCarousel ProposalDetail --> ProposalVotingCard ProposalDetail --> ProposalExportCard ProposalDetail --> MarkdownUtil ProposalExportCard --> ExportUtil ``` ### Data Models / Contracts ```typescript // src/utils/markdown.ts export function renderMarkdown(markdown?: string | null): string; // src/utils/proposalExport.ts export function sanitizeFilename(name: string): string; export async function downloadProposalMarkdown(proposalId: number): Promise; ``` ### Risks - **Styling or Layout Breakage**: Moving aside rail or article container into subcomponents could disrupt CSS grid or sticky positioning. - *Mitigation*: Preserve exact Tailwind classes (`grid xl:grid-cols-[minmax(0,1fr)_15rem]`, `xl:sticky xl:top-6`) on the container and child components. - **Event / Props Wiring**: Ensure proposal ID and markdown download methods properly receive required props. - *Mitigation*: Strict TypeScript interfaces for all component props. # Testing ### Validation Approach Verify the refactored components through type checking, build verification, and functional validation against key user workflows. ### Key Scenarios 1. **Initial Page Load & Empty State**: - On navigating to `/`, proposals list loads in sidebar. - Main content area displays `ProposalEmptyState` ("Select a proposal"). 2. **Proposal Selection**: - Clicking a proposal in the sidebar loads details and renders `ProposalDetail`. - Title, author badge, creator wallet, and paragraphs are displayed with correct markdown formatting. - `AmendmentCarousel` receives the selected proposal ID and displays amendments. 3. **Proposal Export / Download**: - Clicking the download button in `ProposalExportCard` generates and downloads the formatted `.md` file with sanitized filename. 4. **Voting Actions**: - Approve, Reject, and Abstain buttons in `ProposalVotingCard` render properly with hover and focus states. 5. **Mobile Responsiveness**: - Sidebar toggle and overlay function on small screens; sidebar closes upon selecting a proposal. ### Edge Cases - **Missing or Empty Proposal Paragraphs**: `ProposalDetail` handles empty paragraph list gracefully without throwing runtime errors. - **Unsafe HTML in Markdown**: `DOMPurify` via `markdown.ts` sanitizes all rendered HTML in both `ProposalDetail` and `ProposalContainer`. - **Special Characters in Proposal Title**: `sanitizeFilename` replaces forbidden filesystem characters with dashes before triggering file download. ### Test Changes - Execute `npm run build` (`vue-tsc -b && vite build`) to ensure 0 TypeScript or template compilation errors. # Delivery Steps ### ✓ Step 1: Extract markdown formatting and export utilities Create shared utilities to eliminate duplicate markdown handling and file export logic across components. - Create `src/utils/markdown.ts` exporting `renderMarkdown` / `applyFormatting` using `marked` and `DOMPurify`. - Create `src/utils/proposalExport.ts` to encapsulate the Markdown file generator, string sanitization (`sanitizeFilename`), and browser download trigger. - Refactor `src/components/ProposalContainer.vue` to use the shared markdown helper instead of local inline parsing. ### ✓ Step 2: Implement standalone proposal action and state components Create focused components for proposal actions and state placeholders. - Create `src/components/ProposalVotingCard.vue` containing the "Cast your vote" card with Approve, Reject, and Abstain action buttons and emit events. - Create `src/components/ProposalExportCard.vue` containing the Markdown download action and integration with `proposalExport.ts`. - Create `src/components/ProposalEmptyState.vue` displaying the empty selection message and guidance when no proposal is active. ### ✓ Step 3: Implement ProposalDetail component Encapsulate proposal content presentation into a dedicated detail component. - Create `src/components/ProposalDetail.vue` accepting a `proposal: ProposalWithParagraphs` prop. - Include the proposal header (tags, creator attribution, title), content body rendering paragraphs via `applyFormatting`, `AmendmentCarousel` integration, and comments section. - Compose `ProposalVotingCard` and `ProposalExportCard` into the sticky action sidebar rail. ### ✓ Step 4: Refactor HomeView and verify integration Streamline HomeView to act purely as the top-level page orchestrator. - Replace inline proposal markup and action rail in `src/views/HomeView.vue` with `ProposalDetail` and `ProposalEmptyState`. - Clean up unused imports, methods (`applyFormatting`, `downloadProposalMarkdown`, `sanitizeFilename`), and redundant styles from `HomeView.vue`. - Verify TypeScript compilation and runtime behavior with `vue-tsc -b` and `vite build`.