mirror of
https://github.com/amitwh/markdown-converter.git
synced 2026-08-02 18:10:18 +05:30
feat(renderer): PrintPreview overlay + App.tsx mount
This commit is contained in:
@@ -1,15 +1,28 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
import { AppShell } from './components/layout/AppShell';
|
import { AppShell } from './components/layout/AppShell';
|
||||||
import { ModalLayer } from './components/modals/ModalLayer';
|
import { ModalLayer } from './components/modals/ModalLayer';
|
||||||
import { Toaster } from './components/ui/sonner';
|
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';
|
import { useWelcomeTrigger } from './hooks/use-welcome-trigger';
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
useWelcomeTrigger();
|
useWelcomeTrigger();
|
||||||
|
const [printOpen, setPrintOpen] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handler = () => setPrintOpen(true);
|
||||||
|
window.addEventListener('mc:print', handler);
|
||||||
|
return () => window.removeEventListener('mc:print', handler);
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<AppShell />
|
<AppShell />
|
||||||
<ModalLayer />
|
<ModalLayer />
|
||||||
<Toaster />
|
<Toaster />
|
||||||
|
<ReplPanel />
|
||||||
|
{printOpen && <PrintPreview onClose={() => setPrintOpen(false)} />}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 = `<!DOCTYPE html><html><head><meta charset="utf-8"><title>${source.title}</title></head><body><pre>${source.source.replace(/[<>&]/g, (c) => ({ '<': '<', '>': '>', '&': '&' }[c] ?? c))}</pre></body></html>`;
|
||||||
|
await ipc.print({ html });
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!source) {
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 z-50 flex flex-col bg-background">
|
||||||
|
<div className="flex h-12 items-center justify-between border-b border-border bg-card/30 px-4">
|
||||||
|
<h2 className="font-semibold">Print preview</h2>
|
||||||
|
<Button variant="ghost" size="icon" onClick={onClose} aria-label="Close">
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-1 items-center justify-center text-muted-foreground">
|
||||||
|
No file open
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 z-50 flex flex-col bg-background">
|
||||||
|
<div className="flex h-12 items-center justify-between border-b border-border bg-card/30 px-4">
|
||||||
|
<h2 className="font-semibold">Print preview — {source.title}</h2>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button variant="outline" size="sm" onClick={handlePrint}>
|
||||||
|
<Printer className="mr-2 h-4 w-4" />
|
||||||
|
Print
|
||||||
|
</Button>
|
||||||
|
<Button variant="ghost" size="icon" onClick={onClose} aria-label="Close">
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 overflow-auto bg-card/20 p-8">
|
||||||
|
<div className="mx-auto max-w-3xl rounded border border-border bg-background p-8 shadow-lg">
|
||||||
|
<MarkdownRenderer source={source.source} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -63,7 +63,7 @@ export function registerMenuCommands(): void {
|
|||||||
useAppStore.getState().setZenMode(!current);
|
useAppStore.getState().setZenMode(!current);
|
||||||
},
|
},
|
||||||
'file.print': () => {
|
'file.print': () => {
|
||||||
/* stub — placeholder for print feature */
|
if (typeof window !== 'undefined') window.dispatchEvent(new CustomEvent('mc:print'));
|
||||||
},
|
},
|
||||||
'git.refresh': () => {
|
'git.refresh': () => {
|
||||||
/* stub — actual refresh is a useEffect in GitStatusPanel */
|
/* stub — actual refresh is a useEffect in GitStatusPanel */
|
||||||
|
|||||||
@@ -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(<PrintPreview onClose={() => {}} />);
|
||||||
|
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(<PrintPreview onClose={onClose} />);
|
||||||
|
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(<PrintPreview onClose={onClose} />);
|
||||||
|
await userEvent.click(screen.getByRole('button', { name: /close/i }));
|
||||||
|
expect(onClose).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user