diff --git a/components/ascii/ASCIIConverter.tsx b/components/ascii/ASCIIConverter.tsx index 7cf896b..7f19ed3 100644 --- a/components/ascii/ASCIIConverter.tsx +++ b/components/ascii/ASCIIConverter.tsx @@ -19,6 +19,7 @@ export function ASCIIConverter() { const [asciiArt, setAsciiArt] = React.useState(''); const [fonts, setFonts] = React.useState([]); const [isLoading, setIsLoading] = React.useState(false); + const commentedTextRef = React.useRef(''); // Load fonts and check URL params on mount React.useEffect(() => { @@ -71,7 +72,7 @@ export function ASCIIConverter() { if (!asciiArt) return; try { - await navigator.clipboard.writeText(asciiArt); + await navigator.clipboard.writeText(commentedTextRef.current || asciiArt); toast.success('Copied to clipboard!'); } catch (error) { console.error('Failed to copy:', error); @@ -83,7 +84,7 @@ export function ASCIIConverter() { const handleDownload = () => { if (!asciiArt) return; - const blob = new Blob([asciiArt], { type: 'text/plain' }); + const blob = new Blob([commentedTextRef.current || asciiArt], { type: 'text/plain' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; @@ -137,6 +138,7 @@ export function ASCIIConverter() { onCopy={handleCopy} onDownload={handleDownload} onShare={handleShare} + onCommentedTextChange={React.useCallback((t: string) => { commentedTextRef.current = t; }, [])} /> diff --git a/components/ascii/FontPreview.tsx b/components/ascii/FontPreview.tsx index 0a16dfa..a68a2c0 100644 --- a/components/ascii/FontPreview.tsx +++ b/components/ascii/FontPreview.tsx @@ -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 [''].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(null); const [textAlign, setTextAlign] = React.useState('left'); const [fontSize, setFontSize] = React.useState<'xs' | 'sm' | 'base'>('sm'); + const [commentStyle, setCommentStyle] = React.useState('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 (
Preview {font && ( - + {font} - + )}
{onCopy && ( - + + + + + Copy to clipboard + )} {onShare && ( - + + + + + Copy shareable URL + )} - + + + + + Export as PNG + {onDownload && ( - + + + + + Download as text file + )}
{/* Controls */}
-
- - - -
+ + -
- {(['xs', 'sm', 'base'] as const).map((s) => ( - - ))} -
+ v && setFontSize(v as 'xs' | 'sm' | 'base')} + variant="outline" + size="sm" + > + + xs + + + sm + + + md + + + + {!isLoading && text && (
@@ -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} ) : ( diff --git a/components/ui/toggle-group.tsx b/components/ui/toggle-group.tsx new file mode 100644 index 0000000..04411e8 --- /dev/null +++ b/components/ui/toggle-group.tsx @@ -0,0 +1,83 @@ +"use client" + +import * as React from "react" +import { type VariantProps } from "class-variance-authority" +import { ToggleGroup as ToggleGroupPrimitive } from "radix-ui" + +import { cn } from "@/lib/utils/index" +import { toggleVariants } from "@/components/ui/toggle" + +const ToggleGroupContext = React.createContext< + VariantProps & { + spacing?: number + } +>({ + size: "default", + variant: "default", + spacing: 0, +}) + +function ToggleGroup({ + className, + variant, + size, + spacing = 0, + children, + ...props +}: React.ComponentProps & + VariantProps & { + spacing?: number + }) { + return ( + + + {children} + + + ) +} + +function ToggleGroupItem({ + className, + children, + variant, + size, + ...props +}: React.ComponentProps & + VariantProps) { + const context = React.useContext(ToggleGroupContext) + + return ( + + {children} + + ) +} + +export { ToggleGroup, ToggleGroupItem } diff --git a/components/ui/toggle.tsx b/components/ui/toggle.tsx new file mode 100644 index 0000000..069323e --- /dev/null +++ b/components/ui/toggle.tsx @@ -0,0 +1,47 @@ +"use client" + +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" +import { Toggle as TogglePrimitive } from "radix-ui" + +import { cn } from "@/lib/utils/index" + +const toggleVariants = cva( + "inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium hover:bg-muted hover:text-muted-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] outline-none transition-[color,box-shadow] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive whitespace-nowrap", + { + variants: { + variant: { + default: "bg-transparent", + outline: + "border border-input bg-transparent shadow-xs hover:bg-accent hover:text-accent-foreground", + }, + size: { + default: "h-9 px-2 min-w-9", + sm: "h-8 px-1.5 min-w-8", + lg: "h-10 px-2.5 min-w-10", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +) + +function Toggle({ + className, + variant, + size, + ...props +}: React.ComponentProps & + VariantProps) { + return ( + + ) +} + +export { Toggle, toggleVariants }