refactor: centralize action/icon button styles across all tools
Extract shared actionBtn and iconBtn constants into lib/utils/styles.ts and replace all 11 local definitions across tool components. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import { Play, Pause, RotateCcw, Square, Circle, Type } from 'lucide-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 { buildCSS } from '@/lib/animate/cssBuilder';
|
||||||
import type { AnimationConfig, PreviewElement } from '@/types/animate';
|
import type { AnimationConfig, PreviewElement } from '@/types/animate';
|
||||||
|
|
||||||
@@ -27,7 +27,7 @@ const ELEMENTS: { value: PreviewElement; icon: React.ReactNode; title: string }[
|
|||||||
{ value: 'text', icon: <Type className="w-3 h-3" />, title: 'Text' },
|
{ value: 'text', icon: <Type className="w-3 h-3" />, 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) =>
|
const pillCls = (active: boolean) =>
|
||||||
cn(
|
cn(
|
||||||
@@ -138,7 +138,7 @@ export function AnimationPreview({ config, element, onElementChange }: Props) {
|
|||||||
onClick={() => animState === 'ended' ? restart() : setAnimState('playing')}
|
onClick={() => animState === 'ended' ? restart() : setAnimState('playing')}
|
||||||
disabled={animState === 'playing'}
|
disabled={animState === 'playing'}
|
||||||
title={animState === 'ended' ? 'Replay' : 'Play'}
|
title={animState === 'ended' ? 'Replay' : 'Play'}
|
||||||
className={actionBtn}
|
className={previewBtn}
|
||||||
>
|
>
|
||||||
<Play className="w-3 h-3" />
|
<Play className="w-3 h-3" />
|
||||||
</button>
|
</button>
|
||||||
@@ -146,11 +146,11 @@ export function AnimationPreview({ config, element, onElementChange }: Props) {
|
|||||||
onClick={() => setAnimState('paused')}
|
onClick={() => setAnimState('paused')}
|
||||||
disabled={animState !== 'playing'}
|
disabled={animState !== 'playing'}
|
||||||
title="Pause"
|
title="Pause"
|
||||||
className={actionBtn}
|
className={previewBtn}
|
||||||
>
|
>
|
||||||
<Pause className="w-3 h-3" />
|
<Pause className="w-3 h-3" />
|
||||||
</button>
|
</button>
|
||||||
<button onClick={restart} title="Restart" className={actionBtn}>
|
<button onClick={restart} title="Restart" className={previewBtn}>
|
||||||
<RotateCcw className="w-3 h-3" />
|
<RotateCcw className="w-3 h-3" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { useRef } from 'react';
|
import { useRef } from 'react';
|
||||||
import { Plus, Trash2 } from 'lucide-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';
|
import type { Keyframe } from '@/types/animate';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -17,11 +17,7 @@ interface Props {
|
|||||||
|
|
||||||
const TICKS = [25, 50, 75];
|
const TICKS = [25, 50, 75];
|
||||||
|
|
||||||
const iconBtn = (disabled?: boolean) =>
|
const timelineBtn = cn(iconBtn, 'w-6 h-6');
|
||||||
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'
|
|
||||||
);
|
|
||||||
|
|
||||||
export function KeyframeTimeline({ keyframes, selectedId, onSelect, onAdd, onDelete, onMove, embedded = false }: Props) {
|
export function KeyframeTimeline({ keyframes, selectedId, onSelect, onAdd, onDelete, onMove, embedded = false }: Props) {
|
||||||
const trackRef = useRef<HTMLDivElement>(null);
|
const trackRef = useRef<HTMLDivElement>(null);
|
||||||
@@ -68,14 +64,14 @@ export function KeyframeTimeline({ keyframes, selectedId, onSelect, onAdd, onDel
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<button onClick={() => onAdd(50)} title="Add at 50%" className={iconBtn()}>
|
<button onClick={() => onAdd(50)} title="Add at 50%" className={timelineBtn}>
|
||||||
<Plus className="w-3 h-3" />
|
<Plus className="w-3 h-3" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => selectedId && onDelete(selectedId)}
|
onClick={() => selectedId && onDelete(selectedId)}
|
||||||
disabled={!selectedId || keyframes.length <= 2}
|
disabled={!selectedId || keyframes.length <= 2}
|
||||||
title="Delete selected"
|
title="Delete selected"
|
||||||
className={iconBtn(!selectedId || keyframes.length <= 2)}
|
className={timelineBtn}
|
||||||
>
|
>
|
||||||
<Trash2 className="w-3 h-3" />
|
<Trash2 className="w-3 h-3" />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
MessageSquareCode,
|
MessageSquareCode,
|
||||||
Type,
|
Type,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { cn } from '@/lib/utils/cn';
|
import { cn, actionBtn } from '@/lib/utils';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
export type CommentStyle = 'none' | '//' | '#' | '--' | ';' | '/* */' | '<!-- -->' | '"""';
|
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 (
|
return (
|
||||||
<div className={cn('glass rounded-xl p-4 flex flex-col gap-3 flex-1 min-h-0 overflow-hidden', className)}>
|
<div className={cn('glass rounded-xl p-4 flex flex-col gap-3 flex-1 min-h-0 overflow-hidden', className)}>
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { ExportMenu } from '@/components/color/ExportMenu';
|
|||||||
import { useColorInfo, useGeneratePalette, useGenerateGradient } from '@/lib/color/api/queries';
|
import { useColorInfo, useGeneratePalette, useGenerateGradient } from '@/lib/color/api/queries';
|
||||||
import { Loader2, Share2, Plus, X, Palette, Layers } from 'lucide-react';
|
import { Loader2, Share2, Plus, X, Palette, Layers } from 'lucide-react';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn, actionBtn } from '@/lib/utils';
|
||||||
import { MobileTabs } from '@/components/ui/mobile-tabs';
|
import { MobileTabs } from '@/components/ui/mobile-tabs';
|
||||||
|
|
||||||
type HarmonyType = 'monochromatic' | 'analogous' | 'complementary' | 'triadic' | 'tetradic';
|
type HarmonyType = 'monochromatic' | 'analogous' | 'complementary' | 'triadic' | 'tetradic';
|
||||||
@@ -32,8 +32,6 @@ const RIGHT_TABS: { value: RightTab; label: string }[] = [
|
|||||||
{ value: 'gradient', label: 'Gradient' },
|
{ 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() {
|
function ColorManipulationContent() {
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import {
|
|||||||
} from '@/lib/color/utils/export';
|
} from '@/lib/color/utils/export';
|
||||||
import { colorAPI } from '@/lib/color/api/client';
|
import { colorAPI } from '@/lib/color/api/client';
|
||||||
import { CodeSnippet } from '@/components/ui/code-snippet';
|
import { CodeSnippet } from '@/components/ui/code-snippet';
|
||||||
import { cn } from '@/lib/utils/cn';
|
import { cn, actionBtn } from '@/lib/utils';
|
||||||
|
|
||||||
interface ExportMenuProps {
|
interface ExportMenuProps {
|
||||||
colors: string[];
|
colors: string[];
|
||||||
@@ -27,8 +27,6 @@ type ColorSpace = 'hex' | 'rgb' | 'hsl' | 'lab' | 'oklab' | 'lch' | 'oklch';
|
|||||||
const selectCls =
|
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';
|
'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) {
|
export function ExportMenu({ colors, className }: ExportMenuProps) {
|
||||||
const [format, setFormat] = useState<ExportFormat>('css');
|
const [format, setFormat] = useState<ExportFormat>('css');
|
||||||
|
|||||||
@@ -12,15 +12,13 @@ import {
|
|||||||
} from '@/lib/color/api/queries';
|
} from '@/lib/color/api/queries';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { Sun, Moon, Droplets, Droplet, RotateCcw, ArrowLeftRight } from 'lucide-react';
|
import { Sun, Moon, Droplets, Droplet, RotateCcw, ArrowLeftRight } from 'lucide-react';
|
||||||
import { cn } from '@/lib/utils/cn';
|
import { cn, actionBtn } from '@/lib/utils';
|
||||||
|
|
||||||
interface ManipulationPanelProps {
|
interface ManipulationPanelProps {
|
||||||
color: string;
|
color: string;
|
||||||
onColorChange: (color: string) => void;
|
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) {
|
export function ManipulationPanel({ color, onColorChange }: ManipulationPanelProps) {
|
||||||
const [lightenAmount, setLightenAmount] = useState(0.2);
|
const [lightenAmount, setLightenAmount] = useState(0.2);
|
||||||
@@ -118,7 +116,7 @@ export function ManipulationPanel({ color, onColorChange }: ManipulationPanelPro
|
|||||||
onValueChange={(vals) => row.setValue(vals[0])}
|
onValueChange={(vals) => row.setValue(vals[0])}
|
||||||
className="flex-1"
|
className="flex-1"
|
||||||
/>
|
/>
|
||||||
<button onClick={row.onApply} disabled={isLoading} className={actionBtn}>
|
<button onClick={row.onApply} disabled={isLoading} className={cn(actionBtn, 'shrink-0')}>
|
||||||
Apply
|
Apply
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -137,7 +135,7 @@ export function ManipulationPanel({ color, onColorChange }: ManipulationPanelPro
|
|||||||
} catch { toast.error('Failed'); }
|
} catch { toast.error('Failed'); }
|
||||||
}}
|
}}
|
||||||
disabled={isLoading}
|
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')}
|
||||||
>
|
>
|
||||||
<ArrowLeftRight className="w-3 h-3" />
|
<ArrowLeftRight className="w-3 h-3" />
|
||||||
Complementary Color
|
Complementary Color
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { generateFaviconSet } from '@/lib/favicon/faviconService';
|
|||||||
import { downloadBlobsAsZip } from '@/lib/media/utils/fileUtils';
|
import { downloadBlobsAsZip } from '@/lib/media/utils/fileUtils';
|
||||||
import type { FaviconSet, FaviconOptions } from '@/types/favicon';
|
import type { FaviconSet, FaviconOptions } from '@/types/favicon';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { cn } from '@/lib/utils/cn';
|
import { cn, actionBtn } from '@/lib/utils';
|
||||||
import { MobileTabs } from '@/components/ui/mobile-tabs';
|
import { MobileTabs } from '@/components/ui/mobile-tabs';
|
||||||
|
|
||||||
type Tab = 'icons' | 'html' | 'manifest';
|
type Tab = 'icons' | 'html' | 'manifest';
|
||||||
@@ -21,8 +21,6 @@ const TABS: { value: Tab; label: string; icon: React.ReactNode }[] = [
|
|||||||
{ value: 'manifest', label: 'Manifest', icon: <Globe className="w-3 h-3" /> },
|
{ value: 'manifest', label: 'Manifest', icon: <Globe className="w-3 h-3" /> },
|
||||||
];
|
];
|
||||||
|
|
||||||
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 =
|
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';
|
'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() {
|
|||||||
<button
|
<button
|
||||||
onClick={handleGenerate}
|
onClick={handleGenerate}
|
||||||
disabled={!sourceFile || isGenerating}
|
disabled={!sourceFile || isGenerating}
|
||||||
className={cn(actionBtn, 'flex-1 py-2.5')}
|
className={cn(actionBtn, 'flex-1 justify-center')}
|
||||||
>
|
>
|
||||||
{isGenerating
|
{isGenerating
|
||||||
? <><Loader2 className="w-3 h-3 animate-spin" /> Generating… {progress}%</>
|
? <><Loader2 className="w-3 h-3 animate-spin" /> Generating… {progress}%</>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { Download, CheckCircle, XCircle, Loader2, Clock, TrendingUp, RefreshCw } from 'lucide-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 { downloadBlob, formatFileSize, generateOutputFilename } from '@/lib/media/utils/fileUtils';
|
||||||
import type { ConversionJob } from '@/types/media';
|
import type { ConversionJob } from '@/types/media';
|
||||||
|
|
||||||
@@ -11,9 +11,6 @@ export interface ConversionPreviewProps {
|
|||||||
onRetry?: () => void;
|
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) {
|
export function ConversionPreview({ job, onRetry }: ConversionPreviewProps) {
|
||||||
const [previewUrl, setPreviewUrl] = React.useState<string | null>(null);
|
const [previewUrl, setPreviewUrl] = React.useState<string | null>(null);
|
||||||
const [elapsedTime, setElapsedTime] = React.useState(0);
|
const [elapsedTime, setElapsedTime] = React.useState(0);
|
||||||
@@ -171,7 +168,7 @@ export function ConversionPreview({ job, onRetry }: ConversionPreviewProps) {
|
|||||||
})()}
|
})()}
|
||||||
|
|
||||||
{/* Download */}
|
{/* Download */}
|
||||||
<button onClick={handleDownload} className={cn(actionBtn, 'w-full')}>
|
<button onClick={handleDownload} className={cn(actionBtn, 'w-full justify-center')}>
|
||||||
<Download className="w-3 h-3" />
|
<Download className="w-3 h-3" />
|
||||||
<span className="truncate min-w-0">{filename}</span>
|
<span className="truncate min-w-0">{filename}</span>
|
||||||
</button>
|
</button>
|
||||||
@@ -187,7 +184,7 @@ export function ConversionPreview({ job, onRetry }: ConversionPreviewProps) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{onRetry && (
|
{onRetry && (
|
||||||
<button onClick={onRetry} className={cn(actionBtn, 'w-full')}>
|
<button onClick={onRetry} className={cn(actionBtn, 'w-full justify-center')}>
|
||||||
<RefreshCw className="w-3 h-3" />
|
<RefreshCw className="w-3 h-3" />
|
||||||
Retry
|
Retry
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -17,13 +17,10 @@ import { addToHistory } from '@/lib/media/storage/history';
|
|||||||
import { downloadBlobsAsZip, generateOutputFilename } from '@/lib/media/utils/fileUtils';
|
import { downloadBlobsAsZip, generateOutputFilename } from '@/lib/media/utils/fileUtils';
|
||||||
import type { ConversionJob, ConversionFormat, ConversionOptions } from '@/types/media';
|
import type { ConversionJob, ConversionFormat, ConversionOptions } from '@/types/media';
|
||||||
import { ShieldCheck, Download, RotateCcw, Loader2 } from 'lucide-react';
|
import { ShieldCheck, Download, RotateCcw, Loader2 } from 'lucide-react';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn, actionBtn } from '@/lib/utils';
|
||||||
|
|
||||||
type MobileTab = 'upload' | 'convert';
|
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 =
|
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';
|
'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() {
|
|||||||
<button
|
<button
|
||||||
onClick={handleConvert}
|
onClick={handleConvert}
|
||||||
disabled={!selectedFiles.length || !outputFormat || isConverting}
|
disabled={!selectedFiles.length || !outputFormat || isConverting}
|
||||||
className={cn(actionBtn, 'flex-1 py-2')}
|
className={cn(actionBtn, 'flex-1 justify-center py-2')}
|
||||||
>
|
>
|
||||||
{isConverting
|
{isConverting
|
||||||
? <><Loader2 className="w-3 h-3 animate-spin" />Converting…</>
|
? <><Loader2 className="w-3 h-3 animate-spin" />Converting…</>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { Copy, Share2, Image as ImageIcon, FileCode, QrCode } from 'lucide-react';
|
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';
|
import type { ExportSize } from '@/types/qrcode';
|
||||||
|
|
||||||
interface QRPreviewProps {
|
interface QRPreviewProps {
|
||||||
@@ -22,8 +22,6 @@ const EXPORT_SIZES: { value: ExportSize; label: string }[] = [
|
|||||||
{ value: 2048, label: '2k' },
|
{ 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({
|
export function QRPreview({
|
||||||
svgString,
|
svgString,
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import { useState, useCallback } from 'react';
|
import { useState, useCallback } from 'react';
|
||||||
import { RefreshCw, Copy, Check, Clock } from 'lucide-react';
|
import { RefreshCw, Copy, Check, Clock } from 'lucide-react';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { cn } from '@/lib/utils/cn';
|
import { cn, actionBtn } from '@/lib/utils';
|
||||||
import { SliderRow } from '@/components/ui/slider-row';
|
import { SliderRow } from '@/components/ui/slider-row';
|
||||||
import { MobileTabs } from '@/components/ui/mobile-tabs';
|
import { MobileTabs } from '@/components/ui/mobile-tabs';
|
||||||
import {
|
import {
|
||||||
@@ -392,7 +392,7 @@ export function RandomGenerator() {
|
|||||||
<button
|
<button
|
||||||
onClick={() => copy()}
|
onClick={() => copy()}
|
||||||
disabled={!output}
|
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 ? <Check className="w-3.5 h-3.5" /> : <Copy className="w-3.5 h-3.5" />}
|
{copied ? <Check className="w-3.5 h-3.5" /> : <Copy className="w-3.5 h-3.5" />}
|
||||||
{copied ? 'Copied' : 'Copy'}
|
{copied ? 'Copied' : 'Copy'}
|
||||||
|
|||||||
@@ -4,3 +4,4 @@ export * from './urlSharing';
|
|||||||
export * from './animations';
|
export * from './animations';
|
||||||
export * from './format';
|
export * from './format';
|
||||||
export * from './time';
|
export * from './time';
|
||||||
|
export * from './styles';
|
||||||
|
|||||||
11
lib/utils/styles.ts
Normal file
11
lib/utils/styles.ts
Normal file
@@ -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';
|
||||||
Reference in New Issue
Block a user