diff --git a/src/renderer/components/modals/TableGeneratorDialog.tsx b/src/renderer/components/modals/TableGeneratorDialog.tsx new file mode 100644 index 0000000..c992986 --- /dev/null +++ b/src/renderer/components/modals/TableGeneratorDialog.tsx @@ -0,0 +1,132 @@ +import { useState } from 'react'; +import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'; +import { Button } from '@/components/ui/button'; +import { Checkbox } from '@/components/ui/checkbox'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { useAppStore } from '@/stores/app-store'; +import { toast } from '@/lib/toast'; + +function pad(s: string, w: number) { + return s.padEnd(w); +} + +function generateTable(rows: number, cols: number, hasHeader: boolean, headers: string[], data: string[][]): string { + const width = cols; + const colWidths: number[] = Array.from({ length: width }, (_, c) => { + const all = [headers[c] ?? '', ...data.map((r) => r[c] ?? '')]; + return Math.max(...all.map((s) => s.length), 3); + }); + const lines: string[] = []; + if (hasHeader) { + lines.push('| ' + Array.from({ length: width }, (_, c) => pad(headers[c] ?? `Col ${c + 1}`, colWidths[c])).join(' | ') + ' |'); + lines.push('| ' + colWidths.map((w) => '-'.repeat(w)).join(' | ') + ' |'); + } + for (const row of data) { + lines.push('| ' + Array.from({ length: width }, (_, c) => pad(row[c] ?? '', colWidths[c])).join(' | ') + ' |'); + } + // Add empty rows if data has fewer + for (let i = data.length; i < rows; i++) { + lines.push('| ' + colWidths.map((w) => pad('', w)).join(' | ') + ' |'); + } + return lines.join('\n'); +} + +export function TableGeneratorDialog() { + const closeModal = useAppStore((s) => s.closeModal); + const [rows, setRows] = useState(3); + const [cols, setCols] = useState(3); + const [hasHeader, setHasHeader] = useState(true); + const [headers, setHeaders] = useState(['Col 1', 'Col 2', 'Col 3']); + + const output = generateTable(rows, cols, hasHeader, headers, []); + + const handleCopy = async () => { + try { + await navigator.clipboard.writeText(output); + toast.success('Copied to clipboard'); + } catch { + toast.error('Failed to copy'); + } + }; + + const updateHeadersCount = (newCols: number) => { + setCols(newCols); + setHeaders((prev) => { + if (prev.length < newCols) return [...prev, ...Array(newCols - prev.length).fill('').map((_, i) => `Col ${prev.length + i + 1}`)]; + return prev.slice(0, newCols); + }); + }; + + return ( + !o && closeModal()}> + + + Table generator + Specify rows × columns to generate a markdown table + +
+
+
+ + setRows(Math.max(1, Math.min(50, Number(e.target.value) || 1)))} + /> +
+
+ + updateHeadersCount(Math.max(1, Math.min(20, Number(e.target.value) || 1)))} + /> +
+
+ + {hasHeader && ( +
+ +
+ {Array.from({ length: cols }, (_, i) => ( + { + const next = [...headers]; + next[i] = e.target.value; + setHeaders(next); + }} + /> + ))} +
+
+ )} +
+ +
+              {output}
+            
+
+
+ + + + +
+
+ ); +} \ No newline at end of file diff --git a/tests/component/modals/TableGeneratorDialog.test.tsx b/tests/component/modals/TableGeneratorDialog.test.tsx new file mode 100644 index 0000000..e1c2f1f --- /dev/null +++ b/tests/component/modals/TableGeneratorDialog.test.tsx @@ -0,0 +1,36 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { TableGeneratorDialog } from '@/components/modals/TableGeneratorDialog'; + +describe('TableGeneratorDialog', () => { + beforeEach(() => { + localStorage.clear(); + }); + + it('renders with rows, cols, and header inputs', () => { + render(); + expect(screen.getByText(/table generator/i)).toBeInTheDocument(); + expect(screen.getByRole('spinbutton', { name: /rows/i })).toBeInTheDocument(); + expect(screen.getByRole('spinbutton', { name: /cols/i })).toBeInTheDocument(); + expect(screen.getByRole('checkbox', { name: /header/i })).toBeInTheDocument(); + }); + + it('generates a markdown table on rows/cols change', async () => { + render(); + const rowsInput = screen.getByRole('spinbutton', { name: /rows/i }); + const colsInput = screen.getByRole('spinbutton', { name: /cols/i }); + await userEvent.clear(rowsInput); + await userEvent.type(rowsInput, '2'); + await userEvent.clear(colsInput); + await userEvent.type(colsInput, '3'); + // Output should have header + separator + 2 rows = 4 lines for a 3-col table with header + const output = screen.getByTestId('table-output'); + expect(output.textContent).toContain('|'); + }); + + it('shows a copy button next to the output', () => { + render(); + expect(screen.getByRole('button', { name: /copy/i })).toBeInTheDocument(); + }); +}); \ No newline at end of file