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