--- phase: 04-review-download-security plan: 02 type: execute wave: 2 depends_on: [04-01] files_modified: - src/utils/downloadFile.ts - src/utils/downloadZip.ts - src/components/wizard/OutputBlock.tsx autonomous: true requirements: [DOWN-01, DOWN-02, DOWN-03, DOWN-04, DOWN-05, DOWN-06, CONF-03] must_haves: truths: - "downloadFile(content, filename) creates a Blob and triggers an anchor click download" - "downloadZip(files, zipName) builds a JSZip archive and triggers a download" - "OutputBlock renders pre-formatted content, a copy button, and an optional download button" - "OutputBlock copy button calls clipboard.writeText with block content" artifacts: - path: "src/utils/downloadFile.ts" provides: "Pure download utility — Blob + createObjectURL + anchor click" exports: ["downloadFile"] - path: "src/utils/downloadZip.ts" provides: "ZIP bundle download utility using JSZip" exports: ["downloadZip"] - path: "src/components/wizard/OutputBlock.tsx" provides: "Reusable code block with copy and optional download buttons" exports: ["OutputBlock", "OutputBlockProps"] key_links: - from: "src/components/wizard/OutputBlock.tsx" to: "src/utils/downloadFile" via: "import { downloadFile }" pattern: "downloadFile" - from: "src/utils/downloadZip.ts" to: "jszip" via: "import JSZip from 'jszip'" pattern: "JSZip" --- Create the three utility/component building blocks that ReviewStep will compose: the file download helper, the ZIP bundle helper (installs jszip), and the reusable OutputBlock component. Purpose: These are pure, independently testable pieces. Isolating them here keeps ReviewStep.tsx focused on layout and state wiring, not download mechanics. Output: src/utils/downloadFile.ts, src/utils/downloadZip.ts, src/components/wizard/OutputBlock.tsx @C:/Users/SebastienQUEROL/.claude/get-shit-done/workflows/execute-plan.md @C:/Users/SebastienQUEROL/.claude/get-shit-done/templates/summary.md @.planning/phases/04-review-download-security/04-RESEARCH.md @src/store/types.ts @src/components/wizard/ReviewStep.test.tsx Expected by ReviewStep.test.tsx: ```typescript // src/utils/downloadFile.ts export function downloadFile(content: string, filename: string): void // src/utils/downloadZip.ts export async function downloadZip( files: { name: string; content: string }[], zipName: string ): Promise // src/components/wizard/OutputBlock.tsx export interface OutputBlockProps { label: string; content: string; filename?: string; // if provided, show download button disabled?: boolean; // gates both copy and download (security gate) } export function OutputBlock(props: OutputBlockProps): JSX.Element ``` Task 1: Create downloadFile.ts and downloadZip.ts utilities src/utils/downloadFile.ts, src/utils/downloadZip.ts - downloadFile creates a Blob with text/plain;charset=utf-8 MIME type, creates an object URL, clicks a hidden anchor, then revokes the URL immediately after click - downloadZip accepts an array of { name, content } entries, adds each to a JSZip instance, generates a blob asynchronously, then triggers download using the same Blob/anchor pattern - Both utilities have no React dependency (pure browser functions) First, install jszip (the only new runtime dependency this phase): ```bash npm install jszip ``` JSZip ships its own TypeScript declarations. Do NOT install @types/jszip (stale, 6 years old). Create src/utils/downloadFile.ts: ```typescript // src/utils/downloadFile.ts // Triggers a browser file download from a string. // Pattern: MDN URL.createObjectURL — https://developer.mozilla.org/en-US/docs/Web/API/URL/createObjectURL_static export function downloadFile(content: string, filename: string): void { const blob = new Blob([content], { type: 'text/plain;charset=utf-8' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; a.click(); URL.revokeObjectURL(url); } ``` Create src/utils/downloadZip.ts: ```typescript // src/utils/downloadZip.ts // Builds a ZIP archive from file entries and triggers download. // Pattern: JSZip official docs — https://stuk.github.io/jszip/documentation/examples/download-zip-file.html import JSZip from 'jszip'; export async function downloadZip( files: { name: string; content: string }[], zipName: string ): Promise { const zip = new JSZip(); for (const { name, content } of files) { zip.file(name, content); } const blob = await zip.generateAsync({ type: 'blob' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = zipName; a.click(); URL.revokeObjectURL(url); } ``` Do NOT create a src/utils/index.ts barrel — ReviewStep will import directly from the specific utility files to keep the mock paths in tests predictable (the test file already uses '../../utils/downloadFile' and '../../utils/downloadZip'). npm run build 2>&1 | tail -10 Both utility files exist and TypeScript compilation succeeds with no errors. jszip appears in package.json dependencies. Task 2: Create OutputBlock.tsx reusable component src/components/wizard/OutputBlock.tsx - OutputBlock renders a labelled section with a pre/code block showing the content - A "Copy" button calls navigator.clipboard.writeText(content); button label changes to "Copied!" for 2 seconds then reverts - When filename prop is provided, a "Download" button calls downloadFile(content, filename) - When disabled prop is true, both Copy and Download buttons are disabled (HTML disabled attribute) — this is the SECU-01 gate applied per-block - Copy and Download buttons are always rendered (never conditionally hidden) — disabled state is visual only Create src/components/wizard/OutputBlock.tsx: ```typescript // src/components/wizard/OutputBlock.tsx // Reusable output block: pre-formatted content + copy button + optional download button. // disabled prop gates both actions (SECU-01 security acknowledgement gate). import { useState } from 'react'; import { downloadFile } from '../../utils/downloadFile'; export interface OutputBlockProps { label: string; content: string; filename?: string; disabled?: boolean; } export function OutputBlock({ label, content, filename, disabled = false }: OutputBlockProps) { const [copied, setCopied] = useState(false); async function handleCopy() { await navigator.clipboard.writeText(content); setCopied(true); setTimeout(() => setCopied(false), 2000); } return (
{label}
{filename && ( )}
            {content}
          
); } ``` Styling uses Tailwind classes consistent with the existing wizard components. The `disabled:opacity-40` class communicates the locked state visually without hiding buttons.
npm test 2>&1 | tail -15 OutputBlock.tsx exists and compiles cleanly. npm test still shows ReviewStep.test.tsx as RED (component does not exist yet) and all other tests GREEN. No new TypeScript errors.
`npm run build` passes. `npm test` shows: reducer.test.ts GREEN, ReviewStep.test.tsx still RED (ReviewStep not yet created — expected), all prior phase tests GREEN. - src/utils/downloadFile.ts exports downloadFile function - src/utils/downloadZip.ts exports downloadZip async function - src/components/wizard/OutputBlock.tsx exports OutputBlock and OutputBlockProps - jszip listed in package.json dependencies - TypeScript compilation clean After completion, create `.planning/phases/04-review-download-security/04-02-SUMMARY.md`