mirror of
https://github.com/amitwh/markdown-converter.git
synced 2026-08-02 18:10:18 +05:30
feat(renderer): ReplPanel (markdown snippet preview)
This commit is contained in:
@@ -0,0 +1,57 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { X } from 'lucide-react';
|
||||||
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { useSettingsStore } from '@/stores/settings-store';
|
||||||
|
import { renderMarkdown } from '@/lib/markdown';
|
||||||
|
|
||||||
|
export function ReplPanel() {
|
||||||
|
const replOpen = useSettingsStore((s) => s.replOpen);
|
||||||
|
const setSetting = useSettingsStore((s) => s.setSetting);
|
||||||
|
const [source, setSource] = useState('# Markdown preview\n\nType here…');
|
||||||
|
const [debouncedSource, setDebouncedSource] = useState(source);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const t = setTimeout(() => setDebouncedSource(source), 300);
|
||||||
|
return () => clearTimeout(t);
|
||||||
|
}, [source]);
|
||||||
|
|
||||||
|
if (!replOpen) return null;
|
||||||
|
|
||||||
|
const html = renderMarkdown(debouncedSource);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
role="region"
|
||||||
|
aria-label="REPL"
|
||||||
|
className="fixed bottom-0 left-0 right-0 z-40 flex h-[30vh] flex-col border-t border-border bg-card/95 shadow-2xl backdrop-blur"
|
||||||
|
data-testid="repl-panel"
|
||||||
|
>
|
||||||
|
<div className="flex h-8 items-center justify-between border-b border-border bg-card/50 px-3 text-xs">
|
||||||
|
<span className="font-semibold">REPL — Markdown snippet preview</span>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
aria-label="Close REPL"
|
||||||
|
onClick={() => setSetting('replOpen', false)}
|
||||||
|
className="h-6 w-6"
|
||||||
|
>
|
||||||
|
<X className="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-1 overflow-hidden">
|
||||||
|
<Textarea
|
||||||
|
value={source}
|
||||||
|
onChange={(e) => setSource(e.target.value)}
|
||||||
|
className="h-full flex-1 resize-none rounded-none border-r border-border font-mono text-xs"
|
||||||
|
aria-label="Markdown source"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className="h-full flex-1 overflow-auto p-3 text-sm"
|
||||||
|
dangerouslySetInnerHTML={{ __html: html }}
|
||||||
|
data-testid="repl-preview"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { describe, it, expect, beforeEach } from 'vitest';
|
||||||
|
import { render, screen, waitFor } from '@testing-library/react';
|
||||||
|
import userEvent from '@testing-library/user-event';
|
||||||
|
import { ReplPanel } from '@/components/tools/ReplPanel';
|
||||||
|
import { useSettingsStore } from '@/stores/settings-store';
|
||||||
|
|
||||||
|
describe('ReplPanel', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
localStorage.clear();
|
||||||
|
useSettingsStore.setState({ ...useSettingsStore.getInitialState(), replOpen: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders the textarea and preview when replOpen is true', () => {
|
||||||
|
render(<ReplPanel />);
|
||||||
|
expect(screen.getByRole('textbox')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText(/repl/i)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not render when replOpen is false', () => {
|
||||||
|
useSettingsStore.setState({ ...useSettingsStore.getInitialState(), replOpen: false });
|
||||||
|
const { container } = render(<ReplPanel />);
|
||||||
|
expect(container.firstChild).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('typing in the textarea updates the preview', async () => {
|
||||||
|
render(<ReplPanel />);
|
||||||
|
const ta = screen.getByRole('textbox');
|
||||||
|
await userEvent.clear(ta);
|
||||||
|
await userEvent.type(ta, '# Hello');
|
||||||
|
// Wait for 300ms debounce to fire
|
||||||
|
await new Promise((r) => setTimeout(r, 400));
|
||||||
|
expect(screen.getByRole('heading', { name: /hello/i })).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user