feat(renderer): ReplPanel (markdown snippet preview)

This commit is contained in:
2026-06-06 08:11:10 +05:30
parent a62511e7ed
commit 324e5676b8
2 changed files with 91 additions and 0 deletions
@@ -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>
);
}
+34
View File
@@ -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();
});
});