feat(renderer): AsciiGeneratorDialog (figlet text-to-ASCII-art)

This commit is contained in:
2026-06-06 07:40:20 +05:30
parent a8f7547c7e
commit efc7709f66
5 changed files with 161 additions and 0 deletions
+33
View File
@@ -47,6 +47,7 @@
"dompurify": "^3.3.1", "dompurify": "^3.3.1",
"electron-store": "^10.1.0", "electron-store": "^10.1.0",
"ffmpeg-static": "^5.3.0", "ffmpeg-static": "^5.3.0",
"figlet": "^1.11.0",
"highlight.js": "^11.11.1", "highlight.js": "^11.11.1",
"html2pdf.js": "^0.14.0", "html2pdf.js": "^0.14.0",
"immer": "^11.1.8", "immer": "^11.1.8",
@@ -76,6 +77,7 @@
"@testing-library/jest-dom": "^6.9.1", "@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2", "@testing-library/react": "^16.3.2",
"@testing-library/user-event": "^14.6.1", "@testing-library/user-event": "^14.6.1",
"@types/figlet": "^1.7.0",
"@types/node": "^25.9.1", "@types/node": "^25.9.1",
"@types/react": "^19.2.16", "@types/react": "^19.2.16",
"@types/react-dom": "^19.2.3", "@types/react-dom": "^19.2.3",
@@ -5737,6 +5739,13 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/figlet": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/@types/figlet/-/figlet-1.7.0.tgz",
"integrity": "sha512-KwrT7p/8Eo3Op/HBSIwGXOsTZKYiM9NpWRBJ5sVjWP/SmlS+oxxRvJht/FNAtliJvja44N3ul1yATgohnVBV0Q==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/fs-extra": { "node_modules/@types/fs-extra": {
"version": "9.0.13", "version": "9.0.13",
"resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz", "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz",
@@ -10241,6 +10250,30 @@
"node": ">= 6" "node": ">= 6"
} }
}, },
"node_modules/figlet": {
"version": "1.11.0",
"resolved": "https://registry.npmjs.org/figlet/-/figlet-1.11.0.tgz",
"integrity": "sha512-EEx3OS/l2bFqcUNN2NM9FPJp8vAMrgbCxsbl2hbcJNNxOEwVe3mEzrhan7TbJQViZa8mMqhihlbCaqD+LyYKTQ==",
"license": "MIT",
"dependencies": {
"commander": "^14.0.0"
},
"bin": {
"figlet": "bin/index.js"
},
"engines": {
"node": ">= 17.0.0"
}
},
"node_modules/figlet/node_modules/commander": {
"version": "14.0.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz",
"integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==",
"license": "MIT",
"engines": {
"node": ">=20"
}
},
"node_modules/file-entry-cache": { "node_modules/file-entry-cache": {
"version": "8.0.0", "version": "8.0.0",
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
+2
View File
@@ -51,6 +51,7 @@
"@testing-library/jest-dom": "^6.9.1", "@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2", "@testing-library/react": "^16.3.2",
"@testing-library/user-event": "^14.6.1", "@testing-library/user-event": "^14.6.1",
"@types/figlet": "^1.7.0",
"@types/node": "^25.9.1", "@types/node": "^25.9.1",
"@types/react": "^19.2.16", "@types/react": "^19.2.16",
"@types/react-dom": "^19.2.3", "@types/react-dom": "^19.2.3",
@@ -118,6 +119,7 @@
"dompurify": "^3.3.1", "dompurify": "^3.3.1",
"electron-store": "^10.1.0", "electron-store": "^10.1.0",
"ffmpeg-static": "^5.3.0", "ffmpeg-static": "^5.3.0",
"figlet": "^1.11.0",
"highlight.js": "^11.11.1", "highlight.js": "^11.11.1",
"html2pdf.js": "^0.14.0", "html2pdf.js": "^0.14.0",
"immer": "^11.1.8", "immer": "^11.1.8",
@@ -0,0 +1,77 @@
import { useState, useEffect } from 'react';
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Label } from '@/components/ui/label';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Textarea } from '@/components/ui/textarea';
import { useAppStore } from '@/stores/app-store';
import { toast } from '@/lib/toast';
import { figletText, FIGLET_FONTS, type FigletFont } from '@/lib/figlet';
export function AsciiGeneratorDialog() {
const closeModal = useAppStore((s) => s.closeModal);
const [text, setText] = useState('Hello');
const [font, setFont] = useState<FigletFont>('Standard');
const [output, setOutput] = useState('');
useEffect(() => {
let cancelled = false;
figletText(text || ' ', font)
.then((result) => { if (!cancelled) setOutput(result); })
.catch(() => { if (!cancelled) setOutput('(render error)'); });
return () => { cancelled = true; };
}, [text, font]);
const handleCopy = async () => {
try {
await navigator.clipboard.writeText(output);
toast.success('Copied to clipboard');
} catch {
toast.error('Failed to copy');
}
};
return (
<Dialog open onOpenChange={(o) => !o && closeModal()}>
<DialogContent aria-describedby="ascii-desc" className="max-w-2xl">
<DialogHeader>
<DialogTitle>ASCII generator</DialogTitle>
<DialogDescription id="ascii-desc">Type text, pick a font, see ASCII art</DialogDescription>
</DialogHeader>
<div className="space-y-3">
<div>
<Label htmlFor="ascii-text">Text</Label>
<Textarea
id="ascii-text"
aria-label="Text"
value={text}
onChange={(e) => setText(e.target.value)}
rows={2}
/>
</div>
<div>
<Label htmlFor="ascii-font">Font</Label>
<Select value={font} onValueChange={(v) => setFont(v as FigletFont)}>
<SelectTrigger id="ascii-font" aria-label="Font"><SelectValue /></SelectTrigger>
<SelectContent>
{FIGLET_FONTS.map((f) => (
<SelectItem key={f} value={f}>{f}</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div>
<Label>Output</Label>
<pre className="overflow-auto rounded border border-border bg-card/30 p-3 text-xs" data-testid="ascii-output">
{output}
</pre>
</div>
</div>
<DialogFooter>
<Button variant="ghost" onClick={closeModal}>Close</Button>
<Button onClick={handleCopy}>Copy</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
+14
View File
@@ -0,0 +1,14 @@
import figlet from 'figlet';
import type { Fonts } from 'figlet';
export const FIGLET_FONTS = ['Standard', 'Big', 'Small', 'Banner', 'Doom', 'Slant', 'Block'] as const;
export type FigletFont = typeof FIGLET_FONTS[number];
export function figletText(text: string, font: FigletFont): Promise<string> {
return new Promise((resolve, reject) => {
figlet.text(text, { font }, (err, result) => {
if (err) reject(err);
else resolve(result ?? '');
});
});
}
@@ -0,0 +1,35 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { AsciiGeneratorDialog } from '@/components/modals/AsciiGeneratorDialog';
vi.mock('@/lib/figlet', () => ({
figletText: vi.fn().mockResolvedValue(' MOCKED ASCII '),
FIGLET_FONTS: ['Standard', 'Big', 'Small', 'Banner', 'Doom', 'Slant', 'Block'],
}));
describe('AsciiGeneratorDialog', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('renders with an input textarea and font selector', () => {
render(<AsciiGeneratorDialog />);
expect(screen.getByText(/ascii generator/i)).toBeInTheDocument();
expect(screen.getByRole('combobox', { name: /font/i })).toBeInTheDocument();
expect(screen.getByRole('textbox', { name: /text/i })).toBeInTheDocument();
});
it('renders the ASCII output as the user types', async () => {
render(<AsciiGeneratorDialog />);
const input = screen.getByRole('textbox', { name: /text/i });
await userEvent.type(input, 'hi');
const pre = await screen.findByTestId('ascii-output');
expect(pre).toHaveTextContent('MOCKED ASCII');
});
it('shows a copy button next to the output', () => {
render(<AsciiGeneratorDialog />);
expect(screen.getByRole('button', { name: /copy/i })).toBeInTheDocument();
});
});