feat: add comment wrapping to ASCII art tool

Add comment style selector (shadcn Select) to wrap generated ASCII art
with language-appropriate comment syntax (// # -- ; /* */ <!-- --> """).
Refactor preview controls to use shadcn ToggleGroup, Tooltip, and Badge.
Alignment is disabled when a comment style is active.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-27 19:11:25 +01:00
parent a400f694fe
commit 695ba434e2
4 changed files with 279 additions and 70 deletions

View File

@@ -5,6 +5,20 @@ import { toPng } from 'html-to-image';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Skeleton } from '@/components/ui/skeleton';
import { Badge } from '@/components/ui/badge';
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from '@/components/ui/tooltip';
import {
Empty,
EmptyDescription,
@@ -12,10 +26,41 @@ import {
EmptyMedia,
EmptyTitle,
} from "@/components/ui/empty"
import { Copy, Download, Share2, Image as ImageIcon, AlignLeft, AlignCenter, AlignRight, Type } from 'lucide-react';
import { Copy, Download, Share2, Image as ImageIcon, AlignLeft, AlignCenter, AlignRight, Type, MessageSquareCode } from 'lucide-react';
import { cn } from '@/lib/utils/cn';
import { toast } from 'sonner';
export type CommentStyle = 'none' | '//' | '#' | '--' | ';' | '/* */' | '<!-- -->' | '"""';
const COMMENT_STYLES: { value: CommentStyle; label: string }[] = [
{ value: 'none', label: 'None' },
{ value: '//', label: '// C, JS, Go' },
{ value: '#', label: '# Python, Shell' },
{ value: '--', label: '-- SQL, Lua' },
{ value: ';', label: '; Lisp, ASM' },
{ value: '/* */', label: '/* */ Block' },
{ value: '<!-- -->', label: '<!-- --> HTML' },
{ value: '"""', label: '""" Docstring' },
];
function applyCommentStyle(text: string, style: CommentStyle): string {
if (style === 'none' || !text) return text;
const lines = text.split('\n');
switch (style) {
case '//':
case '#':
case '--':
case ';':
return lines.map(line => `${style} ${line}`).join('\n');
case '/* */':
return ['/*', ...lines.map(line => ` * ${line}`), ' */'].join('\n');
case '<!-- -->':
return ['<!--', ...lines, '-->'].join('\n');
case '"""':
return ['"""', ...lines, '"""'].join('\n');
}
}
export interface FontPreviewProps {
text: string;
font?: string;
@@ -23,17 +68,25 @@ export interface FontPreviewProps {
onCopy?: () => void;
onDownload?: () => void;
onShare?: () => void;
onCommentedTextChange?: (commentedText: string) => void;
className?: string;
}
type TextAlign = 'left' | 'center' | 'right';
export function FontPreview({ text, font, isLoading, onCopy, onDownload, onShare, className }: FontPreviewProps) {
const lineCount = text ? text.split('\n').length : 0;
const charCount = text ? text.length : 0;
export function FontPreview({ text, font, isLoading, onCopy, onDownload, onShare, onCommentedTextChange, className }: FontPreviewProps) {
const previewRef = React.useRef<HTMLDivElement>(null);
const [textAlign, setTextAlign] = React.useState<TextAlign>('left');
const [fontSize, setFontSize] = React.useState<'xs' | 'sm' | 'base'>('sm');
const [commentStyle, setCommentStyle] = React.useState<CommentStyle>('none');
const commentedText = React.useMemo(() => applyCommentStyle(text, commentStyle), [text, commentStyle]);
const lineCount = commentedText ? commentedText.split('\n').length : 0;
const charCount = commentedText ? commentedText.length : 0;
React.useEffect(() => {
onCommentedTextChange?.(commentedText);
}, [commentedText, onCommentedTextChange]);
const handleExportPNG = async () => {
if (!previewRef.current || !text) return;
@@ -55,92 +108,116 @@ export function FontPreview({ text, font, isLoading, onCopy, onDownload, onShare
toast.error('Failed to export PNG');
}
};
return (
<Card className={cn('relative', className)}>
<CardHeader className="flex flex-row items-center justify-between flex-wrap gap-2">
<div className="flex items-center gap-2">
<CardTitle>Preview</CardTitle>
{font && (
<span className="text-[10px] px-1.5 py-0.5 bg-primary/10 text-primary rounded font-mono">
<Badge className="text-[10px] font-mono">
{font}
</span>
</Badge>
)}
</div>
<div className="flex gap-1.5 flex-wrap">
{onCopy && (
<Button variant="outline" size="xs" onClick={onCopy}>
<Copy className="h-3 w-3 mr-1" />
Copy
</Button>
<Tooltip>
<TooltipTrigger asChild>
<Button variant="outline" size="xs" onClick={onCopy}>
<Copy className="h-3 w-3 mr-1" />
Copy
</Button>
</TooltipTrigger>
<TooltipContent>Copy to clipboard</TooltipContent>
</Tooltip>
)}
{onShare && (
<Button variant="outline" size="xs" onClick={onShare} title="Copy shareable URL">
<Share2 className="h-3 w-3 mr-1" />
Share
</Button>
<Tooltip>
<TooltipTrigger asChild>
<Button variant="outline" size="xs" onClick={onShare}>
<Share2 className="h-3 w-3 mr-1" />
Share
</Button>
</TooltipTrigger>
<TooltipContent>Copy shareable URL</TooltipContent>
</Tooltip>
)}
<Button variant="outline" size="xs" onClick={handleExportPNG} title="Export as PNG">
<ImageIcon className="h-3 w-3 mr-1" />
PNG
</Button>
<Tooltip>
<TooltipTrigger asChild>
<Button variant="outline" size="xs" onClick={handleExportPNG}>
<ImageIcon className="h-3 w-3 mr-1" />
PNG
</Button>
</TooltipTrigger>
<TooltipContent>Export as PNG</TooltipContent>
</Tooltip>
{onDownload && (
<Button variant="outline" size="xs" onClick={onDownload}>
<Download className="h-3 w-3 mr-1" />
TXT
</Button>
<Tooltip>
<TooltipTrigger asChild>
<Button variant="outline" size="xs" onClick={onDownload}>
<Download className="h-3 w-3 mr-1" />
TXT
</Button>
</TooltipTrigger>
<TooltipContent>Download as text file</TooltipContent>
</Tooltip>
)}
</div>
</CardHeader>
<CardContent className="space-y-3">
{/* Controls */}
<div className="flex items-center gap-2 flex-wrap">
<div className="flex items-center border rounded-md p-0.5">
<button
onClick={() => setTextAlign('left')}
className={cn(
'p-1 rounded transition-colors',
textAlign === 'left' ? 'bg-accent' : 'hover:bg-accent/50'
)}
title="Align left"
>
<ToggleGroup
type="single"
value={textAlign}
onValueChange={(v) => v && setTextAlign(v as TextAlign)}
variant="outline"
size="sm"
disabled={commentStyle !== 'none'}
>
<ToggleGroupItem value="left" aria-label="Align left" className="px-1.5">
<AlignLeft className="h-3 w-3" />
</button>
<button
onClick={() => setTextAlign('center')}
className={cn(
'p-1 rounded transition-colors',
textAlign === 'center' ? 'bg-accent' : 'hover:bg-accent/50'
)}
title="Align center"
>
</ToggleGroupItem>
<ToggleGroupItem value="center" aria-label="Align center" className="px-1.5">
<AlignCenter className="h-3 w-3" />
</button>
<button
onClick={() => setTextAlign('right')}
className={cn(
'p-1 rounded transition-colors',
textAlign === 'right' ? 'bg-accent' : 'hover:bg-accent/50'
)}
title="Align right"
>
</ToggleGroupItem>
<ToggleGroupItem value="right" aria-label="Align right" className="px-1.5">
<AlignRight className="h-3 w-3" />
</button>
</div>
</ToggleGroupItem>
</ToggleGroup>
<div className="flex items-center border rounded-md p-0.5">
{(['xs', 'sm', 'base'] as const).map((s) => (
<button
key={s}
onClick={() => setFontSize(s)}
className={cn(
'px-1.5 py-0.5 text-[10px] rounded transition-colors uppercase',
fontSize === s ? 'bg-accent' : 'hover:bg-accent/50'
)}
>
{s === 'base' ? 'md' : s}
</button>
))}
</div>
<ToggleGroup
type="single"
value={fontSize}
onValueChange={(v) => v && setFontSize(v as 'xs' | 'sm' | 'base')}
variant="outline"
size="sm"
>
<ToggleGroupItem value="xs" aria-label="Extra small font" className="px-1.5 text-[10px] uppercase">
xs
</ToggleGroupItem>
<ToggleGroupItem value="sm" aria-label="Small font" className="px-1.5 text-[10px] uppercase">
sm
</ToggleGroupItem>
<ToggleGroupItem value="base" aria-label="Medium font" className="px-1.5 text-[10px] uppercase">
md
</ToggleGroupItem>
</ToggleGroup>
<Select value={commentStyle} onValueChange={(v) => setCommentStyle(v as CommentStyle)}>
<SelectTrigger size="sm" className="h-8 w-auto gap-1 text-xs">
<MessageSquareCode className="h-3 w-3 text-foreground shrink-0" />
<SelectValue />
</SelectTrigger>
<SelectContent>
{COMMENT_STYLES.map((s) => (
<SelectItem key={s.value} value={s.value}>
{s.label}
</SelectItem>
))}
</SelectContent>
</Select>
{!isLoading && text && (
<div className="flex gap-2 text-[10px] text-muted-foreground ml-auto">
@@ -154,8 +231,8 @@ export function FontPreview({ text, font, isLoading, onCopy, onDownload, onShare
ref={previewRef}
className={cn(
'relative min-h-[200px] bg-muted/50 rounded-lg p-4 overflow-x-auto',
textAlign === 'center' && 'text-center',
textAlign === 'right' && 'text-right'
commentStyle === 'none' && textAlign === 'center' && 'text-center',
commentStyle === 'none' && textAlign === 'right' && 'text-right'
)}
>
{isLoading ? (
@@ -174,7 +251,7 @@ export function FontPreview({ text, font, isLoading, onCopy, onDownload, onShare
fontSize === 'sm' && 'text-xs sm:text-sm',
fontSize === 'base' && 'text-sm sm:text-base'
)}>
{text}
{commentedText}
</pre>
) : (
<Empty>