From 8822c9a2f9162c5418ae6bc8672e9f184c91f411 Mon Sep 17 00:00:00 2001 From: Amit Haridas Date: Sat, 6 Jun 2026 08:11:15 +0530 Subject: [PATCH] feat(renderer): PrintPreview overlay + App.tsx mount --- src/renderer/App.tsx | 13 +++++ .../components/tools/PrintPreview.tsx | 57 +++++++++++++++++++ .../lib/commands/register-menu-commands.ts | 2 +- tests/component/tools/PrintPreview.test.tsx | 49 ++++++++++++++++ 4 files changed, 120 insertions(+), 1 deletion(-) create mode 100644 src/renderer/components/tools/PrintPreview.tsx create mode 100644 tests/component/tools/PrintPreview.test.tsx diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index b1187c6..50a418b 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -1,15 +1,28 @@ +import { useState, useEffect } from 'react'; import { AppShell } from './components/layout/AppShell'; import { ModalLayer } from './components/modals/ModalLayer'; import { Toaster } from './components/ui/sonner'; +import { ReplPanel } from './components/tools/ReplPanel'; +import { PrintPreview } from './components/tools/PrintPreview'; import { useWelcomeTrigger } from './hooks/use-welcome-trigger'; function App() { useWelcomeTrigger(); + const [printOpen, setPrintOpen] = useState(false); + + useEffect(() => { + const handler = () => setPrintOpen(true); + window.addEventListener('mc:print', handler); + return () => window.removeEventListener('mc:print', handler); + }, []); + return ( <> + + {printOpen && setPrintOpen(false)} />} ); } diff --git a/src/renderer/components/tools/PrintPreview.tsx b/src/renderer/components/tools/PrintPreview.tsx new file mode 100644 index 0000000..72e9025 --- /dev/null +++ b/src/renderer/components/tools/PrintPreview.tsx @@ -0,0 +1,57 @@ +import { X, Printer } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { MarkdownRenderer } from '@/components/preview/MarkdownRenderer'; +import { useExportSource } from '@/hooks/use-export-source'; +import { ipc } from '@/lib/ipc'; + +interface Props { + onClose: () => void; +} + +export function PrintPreview({ onClose }: Props) { + const source = useExportSource(); + + const handlePrint = async () => { + if (!source) return; + const html = `${source.title}
${source.source.replace(/[<>&]/g, (c) => ({ '<': '<', '>': '>', '&': '&' }[c] ?? c))}
`; + await ipc.print({ html }); + }; + + if (!source) { + return ( +
+
+

Print preview

+ +
+
+ No file open +
+
+ ); + } + + return ( +
+
+

Print preview — {source.title}

+
+ + +
+
+
+
+ +
+
+
+ ); +} \ No newline at end of file diff --git a/src/renderer/lib/commands/register-menu-commands.ts b/src/renderer/lib/commands/register-menu-commands.ts index 9308371..be4de6a 100644 --- a/src/renderer/lib/commands/register-menu-commands.ts +++ b/src/renderer/lib/commands/register-menu-commands.ts @@ -63,7 +63,7 @@ export function registerMenuCommands(): void { useAppStore.getState().setZenMode(!current); }, 'file.print': () => { - /* stub — placeholder for print feature */ + if (typeof window !== 'undefined') window.dispatchEvent(new CustomEvent('mc:print')); }, 'git.refresh': () => { /* stub — actual refresh is a useEffect in GitStatusPanel */ diff --git a/tests/component/tools/PrintPreview.test.tsx b/tests/component/tools/PrintPreview.test.tsx new file mode 100644 index 0000000..7c5eeaf --- /dev/null +++ b/tests/component/tools/PrintPreview.test.tsx @@ -0,0 +1,49 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { PrintPreview } from '@/components/tools/PrintPreview'; +import { useFileStore } from '@/stores/file-store'; +import { useEditorStore } from '@/stores/editor-store'; + +vi.mock('@/lib/ipc', () => ({ + ipc: { + print: vi.fn().mockResolvedValue({ ok: true }), + }, +})); + +import { ipc } from '@/lib/ipc'; + +describe('PrintPreview', () => { + beforeEach(() => { + localStorage.clear(); + vi.clearAllMocks(); + useFileStore.setState({ + activeTabId: '/test.md', + openTabs: [{ id: '/test.md', path: '/test.md', title: 'test.md', dirty: false }], + } as any); + useEditorStore.setState({ + buffers: new Map([['/test.md', { id: '/test.md', path: '/test.md', content: '# hi', dirty: false }]]), + } as any); + }); + + it('renders the buffer content', () => { + render( {}} />); + expect(screen.getByRole('heading', { name: /hi/i })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: /print/i })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: /close/i })).toBeInTheDocument(); + }); + + it('Print button calls ipc.print', async () => { + const onClose = vi.fn(); + render(); + await userEvent.click(screen.getByRole('button', { name: /print/i })); + expect(ipc.print).toHaveBeenCalledTimes(1); + }); + + it('Close button calls onClose', async () => { + const onClose = vi.fn(); + render(); + await userEvent.click(screen.getByRole('button', { name: /close/i })); + expect(onClose).toHaveBeenCalledTimes(1); + }); +}); \ No newline at end of file