diff --git a/components/animate/AnimationPreview.tsx b/components/animate/AnimationPreview.tsx index 55ddbb5..3df2024 100644 --- a/components/animate/AnimationPreview.tsx +++ b/components/animate/AnimationPreview.tsx @@ -2,7 +2,7 @@ import { useEffect, useRef, useState } from 'react'; import { Play, Pause, RotateCcw, Square, Circle, Type } from 'lucide-react'; -import { cn } from '@/lib/utils/cn'; +import { cn, iconBtn } from '@/lib/utils'; import { buildCSS } from '@/lib/animate/cssBuilder'; import type { AnimationConfig, PreviewElement } from '@/types/animate'; @@ -27,7 +27,7 @@ const ELEMENTS: { value: PreviewElement; icon: React.ReactNode; title: string }[ { value: 'text', icon: , title: 'Text' }, ]; -const actionBtn = 'flex items-center justify-center w-7 h-7 glass rounded-md border border-border/30 text-muted-foreground hover:text-primary hover:border-primary/30 transition-all disabled:opacity-40 disabled:cursor-not-allowed'; +const previewBtn = cn(iconBtn, 'w-7 h-7'); const pillCls = (active: boolean) => cn( @@ -138,7 +138,7 @@ export function AnimationPreview({ config, element, onElementChange }: Props) { onClick={() => animState === 'ended' ? restart() : setAnimState('playing')} disabled={animState === 'playing'} title={animState === 'ended' ? 'Replay' : 'Play'} - className={actionBtn} + className={previewBtn} > @@ -146,11 +146,11 @@ export function AnimationPreview({ config, element, onElementChange }: Props) { onClick={() => setAnimState('paused')} disabled={animState !== 'playing'} title="Pause" - className={actionBtn} + className={previewBtn} > - + diff --git a/components/animate/KeyframeTimeline.tsx b/components/animate/KeyframeTimeline.tsx index cfe0580..68bf769 100644 --- a/components/animate/KeyframeTimeline.tsx +++ b/components/animate/KeyframeTimeline.tsx @@ -2,7 +2,7 @@ import { useRef } from 'react'; import { Plus, Trash2 } from 'lucide-react'; -import { cn } from '@/lib/utils/cn'; +import { cn, iconBtn } from '@/lib/utils'; import type { Keyframe } from '@/types/animate'; interface Props { @@ -17,11 +17,7 @@ interface Props { const TICKS = [25, 50, 75]; -const iconBtn = (disabled?: boolean) => - cn( - 'w-6 h-6 flex items-center justify-center rounded-md glass border border-border/30 text-muted-foreground hover:text-primary hover:border-primary/30 transition-all', - disabled && 'opacity-30 cursor-not-allowed pointer-events-none' - ); +const timelineBtn = cn(iconBtn, 'w-6 h-6'); export function KeyframeTimeline({ keyframes, selectedId, onSelect, onAdd, onDelete, onMove, embedded = false }: Props) { const trackRef = useRef(null); @@ -68,14 +64,14 @@ export function KeyframeTimeline({ keyframes, selectedId, onSelect, onAdd, onDel - onAdd(50)} title="Add at 50%" className={iconBtn()}> + onAdd(50)} title="Add at 50%" className={timelineBtn}> selectedId && onDelete(selectedId)} disabled={!selectedId || keyframes.length <= 2} title="Delete selected" - className={iconBtn(!selectedId || keyframes.length <= 2)} + className={timelineBtn} > diff --git a/components/ascii/FontPreview.tsx b/components/ascii/FontPreview.tsx index 158b47b..584b405 100644 --- a/components/ascii/FontPreview.tsx +++ b/components/ascii/FontPreview.tsx @@ -13,7 +13,7 @@ import { MessageSquareCode, Type, } from 'lucide-react'; -import { cn } from '@/lib/utils/cn'; +import { cn, actionBtn } from '@/lib/utils'; import { toast } from 'sonner'; export type CommentStyle = 'none' | '//' | '#' | '--' | ';' | '/* */' | '' | '"""'; @@ -116,9 +116,6 @@ export function FontPreview({ } }; - const actionBtn = - 'flex items-center gap-1 px-2.5 py-1 text-xs glass rounded-md border border-border/30 text-muted-foreground hover:text-primary hover:border-primary/30 hover:bg-primary/10 transition-all'; - return ( diff --git a/components/color/ColorManipulation.tsx b/components/color/ColorManipulation.tsx index cb54d38..b4d5075 100644 --- a/components/color/ColorManipulation.tsx +++ b/components/color/ColorManipulation.tsx @@ -10,7 +10,7 @@ import { ExportMenu } from '@/components/color/ExportMenu'; import { useColorInfo, useGeneratePalette, useGenerateGradient } from '@/lib/color/api/queries'; import { Loader2, Share2, Plus, X, Palette, Layers } from 'lucide-react'; import { toast } from 'sonner'; -import { cn } from '@/lib/utils'; +import { cn, actionBtn } from '@/lib/utils'; import { MobileTabs } from '@/components/ui/mobile-tabs'; type HarmonyType = 'monochromatic' | 'analogous' | 'complementary' | 'triadic' | 'tetradic'; @@ -32,8 +32,6 @@ const RIGHT_TABS: { value: RightTab; label: string }[] = [ { value: 'gradient', label: 'Gradient' }, ]; -const actionBtn = - 'flex items-center gap-1 px-2.5 py-1 text-xs glass rounded-md border border-border/30 text-muted-foreground hover:text-primary hover:border-primary/30 hover:bg-primary/10 transition-all disabled:opacity-40 disabled:cursor-not-allowed'; function ColorManipulationContent() { const searchParams = useSearchParams(); diff --git a/components/color/ExportMenu.tsx b/components/color/ExportMenu.tsx index 9522452..a424b1f 100644 --- a/components/color/ExportMenu.tsx +++ b/components/color/ExportMenu.tsx @@ -14,7 +14,7 @@ import { } from '@/lib/color/utils/export'; import { colorAPI } from '@/lib/color/api/client'; import { CodeSnippet } from '@/components/ui/code-snippet'; -import { cn } from '@/lib/utils/cn'; +import { cn, actionBtn } from '@/lib/utils'; interface ExportMenuProps { colors: string[]; @@ -27,8 +27,6 @@ type ColorSpace = 'hex' | 'rgb' | 'hsl' | 'lab' | 'oklab' | 'lch' | 'oklch'; const selectCls = 'flex-1 bg-transparent border border-border/40 rounded-lg px-2.5 py-1.5 text-xs font-mono outline-none focus:border-primary/50 transition-colors text-foreground/80 cursor-pointer'; -const actionBtn = - 'flex items-center gap-1.5 px-3 py-1.5 text-xs glass rounded-md border border-border/30 text-muted-foreground hover:text-primary hover:border-primary/30 hover:bg-primary/10 transition-all disabled:opacity-40 disabled:cursor-not-allowed'; export function ExportMenu({ colors, className }: ExportMenuProps) { const [format, setFormat] = useState('css'); diff --git a/components/color/ManipulationPanel.tsx b/components/color/ManipulationPanel.tsx index a8f531a..f79aeb2 100644 --- a/components/color/ManipulationPanel.tsx +++ b/components/color/ManipulationPanel.tsx @@ -12,15 +12,13 @@ import { } from '@/lib/color/api/queries'; import { toast } from 'sonner'; import { Sun, Moon, Droplets, Droplet, RotateCcw, ArrowLeftRight } from 'lucide-react'; -import { cn } from '@/lib/utils/cn'; +import { cn, actionBtn } from '@/lib/utils'; interface ManipulationPanelProps { color: string; onColorChange: (color: string) => void; } -const actionBtn = - 'shrink-0 px-3 py-1 text-[10px] font-mono glass rounded-md border border-border/30 text-muted-foreground hover:text-primary hover:border-primary/30 hover:bg-primary/10 transition-all disabled:opacity-40 disabled:cursor-not-allowed'; export function ManipulationPanel({ color, onColorChange }: ManipulationPanelProps) { const [lightenAmount, setLightenAmount] = useState(0.2); @@ -118,7 +116,7 @@ export function ManipulationPanel({ color, onColorChange }: ManipulationPanelPro onValueChange={(vals) => row.setValue(vals[0])} className="flex-1" /> - + Apply @@ -137,7 +135,7 @@ export function ManipulationPanel({ color, onColorChange }: ManipulationPanelPro } catch { toast.error('Failed'); } }} disabled={isLoading} - className={cn(actionBtn, 'w-full justify-center flex items-center gap-1.5 py-2')} + className={cn(actionBtn, 'w-full justify-center py-2')} > Complementary Color diff --git a/components/favicon/FaviconGenerator.tsx b/components/favicon/FaviconGenerator.tsx index 97271a9..9383043 100644 --- a/components/favicon/FaviconGenerator.tsx +++ b/components/favicon/FaviconGenerator.tsx @@ -9,7 +9,7 @@ import { generateFaviconSet } from '@/lib/favicon/faviconService'; import { downloadBlobsAsZip } from '@/lib/media/utils/fileUtils'; import type { FaviconSet, FaviconOptions } from '@/types/favicon'; import { toast } from 'sonner'; -import { cn } from '@/lib/utils/cn'; +import { cn, actionBtn } from '@/lib/utils'; import { MobileTabs } from '@/components/ui/mobile-tabs'; type Tab = 'icons' | 'html' | 'manifest'; @@ -21,8 +21,6 @@ const TABS: { value: Tab; label: string; icon: React.ReactNode }[] = [ { value: 'manifest', label: 'Manifest', icon: }, ]; -const actionBtn = - 'flex items-center justify-center gap-1.5 px-3 py-1.5 text-xs glass rounded-md border border-border/30 text-muted-foreground hover:text-primary hover:border-primary/30 hover:bg-primary/10 transition-all disabled:opacity-40 disabled:cursor-not-allowed'; const inputCls = 'w-full bg-transparent border border-border/40 rounded-lg px-3 py-2 text-xs font-mono outline-none focus:border-primary/50 transition-colors text-foreground/80 placeholder:text-muted-foreground/30'; @@ -160,7 +158,7 @@ export function FaviconGenerator() { {isGenerating ? <> Generating… {progress}%> diff --git a/components/media/ConversionPreview.tsx b/components/media/ConversionPreview.tsx index 2a259e1..5f61d3f 100644 --- a/components/media/ConversionPreview.tsx +++ b/components/media/ConversionPreview.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import { Download, CheckCircle, XCircle, Loader2, Clock, TrendingUp, RefreshCw } from 'lucide-react'; -import { cn } from '@/lib/utils/cn'; +import { cn, actionBtn } from '@/lib/utils'; import { downloadBlob, formatFileSize, generateOutputFilename } from '@/lib/media/utils/fileUtils'; import type { ConversionJob } from '@/types/media'; @@ -11,9 +11,6 @@ export interface ConversionPreviewProps { onRetry?: () => void; } -const actionBtn = - 'flex items-center justify-center gap-1.5 px-3 py-1.5 text-xs glass rounded-md border border-border/30 text-muted-foreground hover:text-primary hover:border-primary/30 hover:bg-primary/10 transition-all disabled:opacity-40 disabled:cursor-not-allowed'; - export function ConversionPreview({ job, onRetry }: ConversionPreviewProps) { const [previewUrl, setPreviewUrl] = React.useState(null); const [elapsedTime, setElapsedTime] = React.useState(0); @@ -171,7 +168,7 @@ export function ConversionPreview({ job, onRetry }: ConversionPreviewProps) { })()} {/* Download */} - + {filename} @@ -187,7 +184,7 @@ export function ConversionPreview({ job, onRetry }: ConversionPreviewProps) { )} {onRetry && ( - + Retry diff --git a/components/media/FileConverter.tsx b/components/media/FileConverter.tsx index b7bc9e3..8db9d40 100644 --- a/components/media/FileConverter.tsx +++ b/components/media/FileConverter.tsx @@ -17,13 +17,10 @@ import { addToHistory } from '@/lib/media/storage/history'; import { downloadBlobsAsZip, generateOutputFilename } from '@/lib/media/utils/fileUtils'; import type { ConversionJob, ConversionFormat, ConversionOptions } from '@/types/media'; import { ShieldCheck, Download, RotateCcw, Loader2 } from 'lucide-react'; -import { cn } from '@/lib/utils'; +import { cn, actionBtn } from '@/lib/utils'; type MobileTab = 'upload' | 'convert'; -const actionBtn = - 'flex items-center justify-center gap-1.5 px-3 py-1.5 text-xs glass rounded-md border border-border/30 text-muted-foreground hover:text-primary hover:border-primary/30 hover:bg-primary/10 transition-all disabled:opacity-40 disabled:cursor-not-allowed'; - const selectCls = 'w-full bg-transparent border border-border/40 rounded-lg px-2.5 py-1.5 text-xs font-mono outline-none focus:border-primary/50 transition-colors text-foreground/80 cursor-pointer disabled:opacity-40'; @@ -439,7 +436,7 @@ export function FileConverter() { {isConverting ? <>Converting…> diff --git a/components/qrcode/QRPreview.tsx b/components/qrcode/QRPreview.tsx index c102fc9..23bc0b0 100644 --- a/components/qrcode/QRPreview.tsx +++ b/components/qrcode/QRPreview.tsx @@ -1,7 +1,7 @@ 'use client'; import { Copy, Share2, Image as ImageIcon, FileCode, QrCode } from 'lucide-react'; -import { cn } from '@/lib/utils/cn'; +import { cn, actionBtn } from '@/lib/utils'; import type { ExportSize } from '@/types/qrcode'; interface QRPreviewProps { @@ -22,8 +22,6 @@ const EXPORT_SIZES: { value: ExportSize; label: string }[] = [ { value: 2048, label: '2k' }, ]; -const actionBtn = - 'flex items-center gap-1 px-2.5 py-1 text-xs glass rounded-md border border-border/30 text-muted-foreground hover:text-primary hover:border-primary/30 hover:bg-primary/10 transition-all disabled:opacity-40 disabled:cursor-not-allowed'; export function QRPreview({ svgString, diff --git a/components/random/RandomGenerator.tsx b/components/random/RandomGenerator.tsx index 1fda63f..2a66a75 100644 --- a/components/random/RandomGenerator.tsx +++ b/components/random/RandomGenerator.tsx @@ -3,7 +3,7 @@ import { useState, useCallback } from 'react'; import { RefreshCw, Copy, Check, Clock } from 'lucide-react'; import { toast } from 'sonner'; -import { cn } from '@/lib/utils/cn'; +import { cn, actionBtn } from '@/lib/utils'; import { SliderRow } from '@/components/ui/slider-row'; import { MobileTabs } from '@/components/ui/mobile-tabs'; import { @@ -392,7 +392,7 @@ export function RandomGenerator() { copy()} disabled={!output} - className="flex items-center gap-1.5 px-3 py-2 rounded-lg glass border border-border/30 text-xs font-mono text-muted-foreground hover:text-primary hover:border-primary/30 hover:bg-primary/10 transition-all disabled:opacity-30" + className={actionBtn} > {copied ? : } {copied ? 'Copied' : 'Copy'} diff --git a/lib/utils/index.ts b/lib/utils/index.ts index 3eae037..57b03f0 100644 --- a/lib/utils/index.ts +++ b/lib/utils/index.ts @@ -4,3 +4,4 @@ export * from './urlSharing'; export * from './animations'; export * from './format'; export * from './time'; +export * from './styles'; diff --git a/lib/utils/styles.ts b/lib/utils/styles.ts new file mode 100644 index 0000000..8d902d4 --- /dev/null +++ b/lib/utils/styles.ts @@ -0,0 +1,11 @@ +/** + * Shared Tailwind class strings for consistent UI patterns across tools. + */ + +/** Standard action button used throughout all tools (copy, download, share, apply…) */ +export const actionBtn = + 'flex items-center gap-1.5 px-3 py-1.5 text-xs font-mono glass rounded-lg border border-border/30 text-muted-foreground hover:text-primary hover:border-primary/30 hover:bg-primary/10 transition-all disabled:opacity-40 disabled:cursor-not-allowed'; + +/** Small square icon-only button (animate preview controls, timeline actions) */ +export const iconBtn = + 'flex items-center justify-center glass rounded-lg border border-border/30 text-muted-foreground hover:text-primary hover:border-primary/30 hover:bg-primary/10 transition-all disabled:opacity-40 disabled:cursor-not-allowed';