Added mobile support

This commit is contained in:
noah
2026-03-09 22:06:58 +01:00
parent 76f02c3b1a
commit 18912547a8
3 changed files with 389 additions and 237 deletions
+45 -42
View File
@@ -64,15 +64,15 @@ export default function ConvertPage() {
/>
{/* Finder Window — the entire converter lives inside this */}
<div className="relative z-10 max-w-[960px] mx-auto px-4 sm:px-6 py-6 sm:py-10">
<div className="relative z-10 max-w-[960px] mx-auto px-2 sm:px-6 py-3 sm:py-10">
<motion.div
className="flex flex-col max-h-[calc(100vh-80px)] rounded-xl overflow-hidden shadow-[0_12px_60px_rgba(45,31,20,0.12)] border border-border-soft bg-white"
className="flex flex-col max-h-[calc(100vh-24px)] sm:max-h-[calc(100vh-80px)] rounded-xl overflow-hidden shadow-[0_12px_60px_rgba(45,31,20,0.12)] border border-border-soft bg-white"
initial={{ opacity: 0, y: 16, scale: 0.98 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
transition={{ duration: 0.5, ease: [0.16, 1, 0.3, 1] as const }}
>
{/* ─ Title bar ─ */}
<div className="flex-shrink-0 flex items-center gap-3 px-4 py-2.5 bg-[#f6f6f6] border-b border-border-soft select-none">
<div className="flex-shrink-0 flex items-center gap-2 sm:gap-3 px-3 sm:px-4 py-2 sm:py-2.5 bg-[#f6f6f6] border-b border-border-soft select-none">
{/* Traffic lights */}
<Link href="/" className="flex items-center gap-[6px] no-underline group/dots">
<div className="w-[11px] h-[11px] rounded-full bg-[#ff5f57] border border-[#e0443e]/40 group-hover/dots:bg-[#ff3b30] transition-colors" />
@@ -80,8 +80,8 @@ export default function ConvertPage() {
<div className="w-[11px] h-[11px] rounded-full bg-[#28c840] border border-[#1aab29]/40 group-hover/dots:bg-[#28cd41] transition-colors" />
</Link>
{/* Navigation arrows */}
<div className="flex items-center gap-1 ml-1">
{/* Navigation arrows — hidden on mobile */}
<div className="hidden sm:flex items-center gap-1 ml-1">
<Link href="/" className="text-text-light/40 hover:text-text-mid transition-colors no-underline">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M15 18l-6-6 6-6" /></svg>
</Link>
@@ -94,8 +94,8 @@ export default function ConvertPage() {
<div className="flex-1 flex items-center justify-center gap-1.5">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img src="/logo.png" alt="" className="w-4 h-4 rounded-[4px]" />
<span className="text-[12px] font-medium text-text-mid">Transmute</span>
<span className="text-[12px] text-text-light/40">{'\u203A'}</span>
<span className="text-[12px] font-medium text-text-mid hidden sm:inline">Transmute</span>
<span className="text-[12px] text-text-light/40 hidden sm:inline">{'\u203A'}</span>
<span className="text-[12px] font-medium text-text-dark">Converter</span>
</div>
@@ -120,8 +120,8 @@ export default function ConvertPage() {
{/* ─ Toolbar (only when files present) ─ */}
{hasFiles && (
<div className="flex-shrink-0 flex items-center justify-between px-4 py-2 bg-[#fafafa] border-b border-border-soft">
<div className="flex items-center gap-3">
<div className="flex-shrink-0 flex items-center justify-between px-3 sm:px-4 py-1.5 sm:py-2 bg-[#fafafa] border-b border-border-soft">
<div className="flex items-center gap-2 sm:gap-3">
<span className="font-mono text-[11px] text-text-mid">
<strong className="text-text-dark">{files.length}</strong> file{files.length !== 1 ? 's' : ''}
</span>
@@ -130,13 +130,11 @@ export default function ConvertPage() {
{formatFileSize(totalSize)}
</span>
{completedCount > 0 && (
<>
<div className="w-px h-3.5 bg-border-soft" />
<span className="font-mono text-[11px] text-text-mid flex items-center gap-1">
<span className="hidden sm:flex items-center gap-1 font-mono text-[11px] text-text-mid">
<div className="w-px h-3.5 bg-border-soft mr-1" />
<div className="w-1.5 h-1.5 rounded-full bg-mint" />
<strong className="text-mint">{completedCount}</strong> converted
</span>
</>
)}
</div>
<button
@@ -148,12 +146,12 @@ export default function ConvertPage() {
</div>
)}
{/* ─ Column headers (only when files present) ─ */}
{/* ─ Column headers (desktop only — mobile uses stacked rows) ─ */}
{hasFiles && (
<div className="flex-shrink-0 flex items-center px-4 py-1.5 bg-[#fafafa] border-b border-border-soft text-[11px] font-medium text-text-light tracking-wide uppercase select-none">
<div className="flex-shrink-0 hidden sm:flex items-center px-4 py-1.5 bg-[#fafafa] border-b border-border-soft text-[11px] font-medium text-text-light tracking-wide uppercase select-none">
<div className="flex-1 pl-10">Name</div>
<div className="w-[72px] text-right hidden sm:block">Size</div>
<div className="w-[140px] text-center hidden sm:block">Convert to</div>
<div className="w-[72px] text-right">Size</div>
<div className="w-[140px] text-center">Convert to</div>
<div className="w-[130px] text-right pr-1">Status</div>
</div>
)}
@@ -165,7 +163,7 @@ export default function ConvertPage() {
onDragLeave={handleDragLeave}
onDragOver={handleDragOver}
onDrop={handleDrop}
style={{ minHeight: hasFiles ? undefined : '60vh' }}
style={{ minHeight: hasFiles ? undefined : '50vh' }}
>
{/* Drop zone (empty state) */}
{!hasFiles && (
@@ -222,34 +220,33 @@ export default function ConvertPage() {
{Array.from({ length: ghostRowCount }).map((_, i) => (
<div
key={`ghost-${i}`}
className={`flex items-center px-4 py-2.5 ${i === 0 ? 'cursor-pointer hover:bg-[#fafafa] transition-colors' : ''}`}
className={`flex items-center px-3 sm:px-4 py-2.5 ${i === 0 ? 'cursor-pointer hover:bg-[#fafafa] active:bg-[#fafafa] transition-colors' : ''} ${i >= 4 ? 'hidden sm:flex' : ''}`}
onClick={i === 0 ? openFilePicker : undefined}
>
{/* Ghost icon */}
<div className="w-8 h-8 rounded-lg bg-border-soft/20 flex-shrink-0" />
<div className="w-8 sm:w-8 h-8 sm:h-8 rounded-lg bg-border-soft/20 flex-shrink-0" />
{/* Ghost name bar */}
<div className="flex-1 ml-3">
<div className="flex-1 ml-2.5 sm:ml-3">
{i === 0 ? (
<span className="text-[12px] text-text-light/40 flex items-center gap-1.5">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" className="opacity-40">
<path d="M12 5v14M5 12h14" strokeLinecap="round" />
</svg>
Drop files here or click to browse
<span className="hidden sm:inline">Drop files here or click to browse</span>
<span className="sm:hidden">Tap to add files</span>
</span>
) : (
<div className="h-2.5 w-24 rounded bg-border-soft/15" style={{ width: `${60 + ((i * 37) % 60)}px` }} />
<div className="h-2.5 rounded bg-border-soft/15" style={{ width: `${60 + ((i * 37) % 60)}px` }} />
)}
</div>
{/* Ghost size */}
{/* Ghost columns — desktop only */}
<div className="w-[72px] hidden sm:block">
{i < 2 && <div className="h-2 w-10 rounded bg-border-soft/10 ml-auto" />}
</div>
{/* Ghost convert to */}
<div className="w-[140px] hidden sm:block">
{i < 2 && <div className="h-2 w-12 rounded bg-border-soft/10 mx-auto" />}
</div>
{/* Ghost status */}
<div className="w-[130px] pr-1">
<div className="w-[130px] pr-1 hidden sm:block">
{i < 1 && <div className="h-2 w-8 rounded bg-border-soft/10 ml-auto" />}
</div>
</div>
@@ -262,40 +259,45 @@ export default function ConvertPage() {
{/* ─ Bottom action bar (always visible) ─ */}
{hasFiles && (
<div className="flex-shrink-0 flex items-center justify-between px-4 py-3 bg-[#fafafa] border-t border-border-soft">
<div className="flex-shrink-0 flex items-center justify-between px-3 sm:px-4 py-2.5 sm:py-3 bg-[#fafafa] border-t border-border-soft gap-2">
{/* Left — status */}
<div className="flex items-center gap-1.5">
<div className="flex items-center gap-1.5 min-w-0">
{isConverting ? (
<>
<motion.div
className="w-3 h-3 rounded-full border-[1.5px] border-pink border-t-transparent"
className="w-3 h-3 rounded-full border-[1.5px] border-pink border-t-transparent flex-shrink-0"
animate={{ rotate: 360 }}
transition={{ duration: 0.6, repeat: Infinity, ease: 'linear' }}
/>
<span className="text-[11px] font-mono text-pink font-medium">Converting...</span>
<span className="text-[11px] font-mono text-pink font-medium truncate">Converting...</span>
</>
) : completedCount === files.length && completedCount > 0 ? (
<>
<div className="w-1.5 h-1.5 rounded-full bg-mint" />
<span className="text-[11px] font-mono text-mint font-medium">All converted</span>
<div className="w-1.5 h-1.5 rounded-full bg-mint flex-shrink-0" />
<span className="text-[11px] font-mono text-mint font-medium truncate">All done</span>
</>
) : (
<span className="text-[11px] text-text-light font-mono">
{convertableCount > 0 ? `${convertableCount} ready to convert` : 'Select target formats'}
<span className="text-[11px] text-text-light font-mono truncate">
{convertableCount > 0 ? (
<>
<span className="hidden sm:inline">{convertableCount} ready to convert</span>
<span className="sm:hidden">{convertableCount} ready</span>
</>
) : 'Select formats'}
</span>
)}
</div>
{/* Right — action buttons */}
<div className="flex items-center gap-2">
<div className="flex items-center gap-1.5 sm:gap-2 flex-shrink-0">
{completedCount > 0 && (
<motion.button
className="inline-flex items-center gap-1.5 px-3.5 py-1.5 text-[12px] font-bold text-white bg-mint border-none rounded-lg cursor-pointer shadow-[0_2px_8px_rgba(52,211,153,0.2)] hover:-translate-y-0.5 hover:shadow-[0_3px_14px_rgba(52,211,153,0.3)] transition-all"
className="inline-flex items-center gap-1 sm:gap-1.5 px-2.5 sm:px-3.5 py-1.5 text-[11px] sm:text-[12px] font-bold text-white bg-mint border-none rounded-lg cursor-pointer shadow-[0_2px_8px_rgba(52,211,153,0.2)] hover:-translate-y-0.5 hover:shadow-[0_3px_14px_rgba(52,211,153,0.3)] transition-all"
onClick={() => downloadAllAsZip(files)}
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
>
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4M7 10l5 5 5-5M12 15V3" />
</svg>
ZIP
@@ -303,20 +305,21 @@ export default function ConvertPage() {
)}
<button
className={`inline-flex items-center gap-1.5 px-5 py-1.5 text-[12px] font-bold text-white bg-pink border-none rounded-lg cursor-pointer shadow-[0_2px_12px_rgba(244,114,182,0.25)] hover:-translate-y-0.5 hover:shadow-[0_4px_18px_rgba(244,114,182,0.35)] transition-all disabled:opacity-40 disabled:cursor-not-allowed disabled:shadow-none disabled:transform-none ${isConverting ? 'animate-pulse-soft opacity-85' : ''}`}
className={`inline-flex items-center gap-1 sm:gap-1.5 px-3.5 sm:px-5 py-1.5 text-[11px] sm:text-[12px] font-bold text-white bg-pink border-none rounded-lg cursor-pointer shadow-[0_2px_12px_rgba(244,114,182,0.25)] hover:-translate-y-0.5 hover:shadow-[0_4px_18px_rgba(244,114,182,0.35)] transition-all disabled:opacity-40 disabled:cursor-not-allowed disabled:shadow-none disabled:transform-none ${isConverting ? 'animate-pulse-soft opacity-85' : ''}`}
onClick={() => convertAll(files)}
disabled={isConverting || convertableCount === 0}
>
{isConverting ? (
<>
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" className="animate-spin">
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" className="animate-spin">
<path d="M12 2v4M12 18v4M4.93 4.93l2.83 2.83M16.24 16.24l2.83 2.83M2 12h4M18 12h4M4.93 19.07l2.83-2.83M16.24 7.76l2.83-2.83" />
</svg>
Converting...
<span className="hidden sm:inline">Converting...</span>
<span className="sm:hidden">Wait...</span>
</>
) : (
<>
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
<path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z" strokeLinecap="round" strokeLinejoin="round" />
</svg>
Transmute{convertableCount > 0 ? ` (${convertableCount})` : ''}
+8 -8
View File
@@ -26,22 +26,22 @@ export function DropZone({
// Empty state inside Finder window
return (
<div
className="flex items-center justify-center px-6 py-16"
style={{ minHeight: '60vh' }}
className="flex items-center justify-center px-4 sm:px-6 py-10 sm:py-16"
style={{ minHeight: '50vh' }}
onDragEnter={onDragEnter}
onDragLeave={onDragLeave}
onDragOver={onDragOver}
onDrop={onDrop}
>
<motion.div
className="flex flex-col items-center gap-4 text-center"
className="flex flex-col items-center gap-3 sm:gap-4 text-center"
initial={{ opacity: 0, y: 16 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, ease: [0.16, 1, 0.3, 1] as const }}
>
{/* Upload icon */}
<motion.div
className={`flex items-center justify-center w-20 h-20 rounded-2xl transition-all duration-300 ${
className={`flex items-center justify-center w-16 h-16 sm:w-20 sm:h-20 rounded-2xl transition-all duration-300 ${
isDragging
? 'bg-pink/12 text-pink scale-110'
: 'bg-[#f6f6f6] text-text-light'
@@ -52,7 +52,7 @@ export function DropZone({
}}
transition={{ type: 'spring', stiffness: 300, damping: 20 }}
>
<svg width="36" height="36" viewBox="0 0 48 48" fill="none">
<svg width="32" height="32" viewBox="0 0 48 48" fill="none" className="sm:w-9 sm:h-9">
<path
d="M24 32V12M24 12L16 20M24 12L32 20"
stroke="currentColor"
@@ -71,10 +71,10 @@ export function DropZone({
</motion.div>
<div>
<h2 className="font-serif text-2xl font-extrabold text-text-dark tracking-tight mb-1">
<h2 className="font-serif text-xl sm:text-2xl font-extrabold text-text-dark tracking-tight mb-1">
{isDragging ? 'Release to add' : 'Drop files here'}
</h2>
<p className="text-text-mid text-[14px] max-w-xs leading-relaxed">
<p className="text-text-mid text-[13px] sm:text-[14px] max-w-xs leading-relaxed">
{isDragging
? 'Your files are ready for transformation'
: 'Images, documents, audio, video, data \u2014 all formats welcome'}
@@ -82,7 +82,7 @@ export function DropZone({
</div>
<motion.button
className="inline-flex items-center gap-2 mt-1 px-6 py-2.5 text-[13px] font-bold text-white bg-pink rounded-xl cursor-pointer shadow-[0_3px_16px_rgba(244,114,182,0.25)] hover:-translate-y-0.5 hover:shadow-[0_5px_22px_rgba(244,114,182,0.35)] transition-all border-none"
className="inline-flex items-center gap-2 mt-1 px-5 sm:px-6 py-2 sm:py-2.5 text-[13px] font-bold text-white bg-pink rounded-xl cursor-pointer shadow-[0_3px_16px_rgba(244,114,182,0.25)] hover:-translate-y-0.5 hover:shadow-[0_5px_22px_rgba(244,114,182,0.35)] active:scale-[0.97] transition-all border-none"
onClick={onBrowse}
whileHover={{ scale: 1.03 }}
whileTap={{ scale: 0.97 }}
+191 -42
View File
@@ -27,13 +27,15 @@ export function FileRow({
return (
<motion.div
className="group flex items-center px-4 py-2.5 hover:bg-[#fafafa] transition-colors"
className="group relative px-3 sm:px-4 py-2.5 hover:bg-[#fafafa] transition-colors"
initial={{ opacity: 0, x: -8 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -8, height: 0, paddingTop: 0, paddingBottom: 0, overflow: 'hidden' }}
transition={{ duration: 0.3, delay: index * 0.03, ease: [0.16, 1, 0.3, 1] as const }}
layout
>
{/* ─── Desktop layout (sm+) ─── */}
<div className="hidden sm:flex items-center">
{/* Icon */}
<div
className="w-8 h-8 rounded-lg flex items-center justify-center text-[15px] flex-shrink-0"
@@ -41,11 +43,7 @@ export function FileRow({
>
{file.preview ? (
// eslint-disable-next-line @next/next/no-img-element
<img
src={file.preview}
alt=""
className="w-8 h-8 rounded-lg object-cover"
/>
<img src={file.preview} alt="" className="w-8 h-8 rounded-lg object-cover" />
) : (
icon
)}
@@ -64,19 +62,18 @@ export function FileRow({
.{file.extension}
</span>
</div>
{/* Error message inline */}
{file.status === 'error' && file.error && (
<p className="text-[10px] text-red-400 truncate mt-0.5">{file.error}</p>
)}
</div>
{/* Size — hidden on small screens */}
<div className="w-[72px] text-right font-mono text-[11px] text-text-light flex-shrink-0 hidden sm:block">
{/* Size */}
<div className="w-[72px] text-right font-mono text-[11px] text-text-light flex-shrink-0">
{formatFileSize(file.size)}
</div>
{/* Format selector — hidden on small screens, shown inline */}
<div className="w-[140px] flex items-center justify-center flex-shrink-0 hidden sm:flex">
{/* Format selector */}
<div className="w-[140px] flex items-center justify-center flex-shrink-0">
{file.availableFormats.length > 0 && file.status !== 'done' ? (
<div className="flex items-center gap-1.5">
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" className="text-text-light/40 flex-shrink-0">
@@ -88,9 +85,7 @@ export function FileRow({
className="font-mono text-[11px] font-bold text-text-dark bg-transparent px-1.5 py-1 rounded-md border border-border-soft/60 cursor-pointer hover:border-pink/30 focus:outline-none focus:border-pink/40 transition-all appearance-none select-arrow-warm w-[72px]"
>
{file.availableFormats.map((fmt) => (
<option key={fmt} value={fmt}>
.{fmt}
</option>
<option key={fmt} value={fmt}>.{fmt}</option>
))}
</select>
</div>
@@ -106,10 +101,96 @@ export function FileRow({
)}
</div>
{/* Status / actions column */}
{/* Status / actions */}
<div className="w-[130px] flex items-center justify-end gap-1.5 flex-shrink-0 pr-1">
<DesktopStatus file={file} color={color} onRemove={onRemove} onDownload={onDownload} onPreview={onPreview} />
</div>
</div>
{/* ─── Mobile layout (< sm) ─── */}
<div className="flex sm:hidden items-start gap-2.5">
{/* Icon */}
<div
className="w-9 h-9 rounded-lg flex items-center justify-center text-[15px] flex-shrink-0 mt-0.5"
style={{ background: `${color}12` }}
>
{file.preview ? (
// eslint-disable-next-line @next/next/no-img-element
<img src={file.preview} alt="" className="w-9 h-9 rounded-lg object-cover" />
) : (
icon
)}
</div>
{/* Content */}
<div className="flex-1 min-w-0">
{/* Row 1: Name + remove button */}
<div className="flex items-center gap-1.5">
<span className="text-[13px] font-medium text-text-dark truncate flex-1" title={file.name}>
{truncateFilename(file.name, 24)}
</span>
<span
className="font-mono text-[9px] font-bold px-1 py-0.5 rounded flex-shrink-0"
style={{ background: `${color}10`, color }}
>
.{file.extension}
</span>
{/* Remove — always visible on mobile */}
{file.status !== 'converting' && (
<button
className="flex items-center justify-center w-6 h-6 rounded-md bg-transparent border-none cursor-pointer text-text-light/40 active:text-red-400 active:bg-red-50 transition-all flex-shrink-0 -mr-1"
onClick={() => onRemove(file.id)}
aria-label="Remove file"
>
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
<path d="M18 6L6 18M6 6l12 12" />
</svg>
</button>
)}
</div>
{/* Row 2: Size + format selector + status */}
<div className="flex items-center gap-2 mt-1.5">
<span className="font-mono text-[10px] text-text-light flex-shrink-0">
{formatFileSize(file.size)}
</span>
{/* Format selector or status */}
<MobileStatus
file={file}
color={color}
onSetFormat={onSetFormat}
onDownload={onDownload}
onPreview={onPreview}
/>
</div>
{/* Error message */}
{file.status === 'error' && file.error && (
<p className="text-[10px] text-red-400 truncate mt-1">{file.error}</p>
)}
</div>
</div>
</motion.div>
);
}
/* ── Desktop status (sm+) ─────────────────────────────── */
function DesktopStatus({
file,
color,
onRemove,
onDownload,
onPreview,
}: {
file: UploadedFile;
color: string;
onRemove: (id: string) => void;
onDownload: (file: UploadedFile) => void;
onPreview: (file: UploadedFile) => void;
}) {
return (
<AnimatePresence mode="wait">
{/* Idle — just format selector on mobile, or nothing */}
{file.status === 'idle' && (
<motion.div
key="idle"
@@ -119,22 +200,6 @@ export function FileRow({
exit={{ opacity: 0 }}
transition={{ duration: 0.15 }}
>
{/* Mobile format selector */}
{file.availableFormats.length > 0 && (
<select
value={file.targetFormat || ''}
onChange={(e) => onSetFormat(file.id, e.target.value)}
className="sm:hidden font-mono text-[10px] font-bold text-text-dark bg-transparent px-1 py-0.5 rounded border border-border-soft/60 cursor-pointer appearance-none select-arrow-warm"
style={{ paddingRight: '18px', maxWidth: '70px' }}
>
{file.availableFormats.map((fmt) => (
<option key={fmt} value={fmt}>
.{fmt}
</option>
))}
</select>
)}
{/* Remove button */}
<button
className="flex items-center justify-center w-6 h-6 rounded-md bg-transparent border-none cursor-pointer text-text-light/30 hover:text-red-400 hover:bg-red-50 transition-all opacity-0 group-hover:opacity-100"
onClick={() => onRemove(file.id)}
@@ -147,7 +212,6 @@ export function FileRow({
</motion.div>
)}
{/* Converting — spinner + progress */}
{file.status === 'converting' && (
<motion.div
key="converting"
@@ -164,7 +228,6 @@ export function FileRow({
</motion.div>
)}
{/* Done — checkmark + preview + download */}
{file.status === 'done' && (
<motion.div
key="done"
@@ -173,13 +236,10 @@ export function FileRow({
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.25, ease: [0.16, 1, 0.3, 1] as const }}
>
{/* Checkmark */}
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" className="text-mint flex-shrink-0">
<circle cx="12" cy="12" r="10" fill="rgba(52,211,153,0.12)" stroke="currentColor" strokeWidth="1.5" />
<path d="M8 12l3 3 5-5" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
</svg>
{/* Preview button */}
<button
className="flex items-center justify-center w-6 h-6 rounded-md bg-transparent border-none cursor-pointer text-text-light/50 hover:text-purple hover:bg-purple/8 transition-all"
onClick={() => onPreview(file)}
@@ -190,8 +250,6 @@ export function FileRow({
<circle cx="12" cy="12" r="3" />
</svg>
</button>
{/* Download button */}
<button
className="inline-flex items-center gap-1 px-2 py-1 text-[10px] font-bold text-white bg-mint border-none rounded-md cursor-pointer shadow-[0_1px_4px_rgba(52,211,153,0.2)] hover:shadow-[0_2px_8px_rgba(52,211,153,0.3)] transition-all"
onClick={() => onDownload(file)}
@@ -205,7 +263,6 @@ export function FileRow({
</motion.div>
)}
{/* Error — icon + remove */}
{file.status === 'error' && (
<motion.div
key="error"
@@ -232,7 +289,99 @@ export function FileRow({
</motion.div>
)}
</AnimatePresence>
</div>
</motion.div>
);
}
/* ── Mobile status (< sm) ─────────────────────────────── */
function MobileStatus({
file,
color,
onSetFormat,
onDownload,
onPreview,
}: {
file: UploadedFile;
color: string;
onSetFormat: (id: string, format: string) => void;
onDownload: (file: UploadedFile) => void;
onPreview: (file: UploadedFile) => void;
}) {
if (file.status === 'idle' && file.availableFormats.length > 0) {
return (
<div className="flex items-center gap-1.5 flex-1 min-w-0">
<svg width="9" height="9" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" className="text-text-light/40 flex-shrink-0">
<path d="M5 12h14M12 5l7 7-7 7" />
</svg>
<select
value={file.targetFormat || ''}
onChange={(e) => onSetFormat(file.id, e.target.value)}
className="font-mono text-[11px] font-bold text-text-dark bg-transparent px-1.5 py-0.5 rounded-md border border-border-soft/60 cursor-pointer focus:outline-none focus:border-pink/40 transition-all appearance-none select-arrow-warm"
style={{ maxWidth: '80px' }}
>
{file.availableFormats.map((fmt) => (
<option key={fmt} value={fmt}>.{fmt}</option>
))}
</select>
</div>
);
}
if (file.status === 'converting') {
return (
<div className="flex items-center gap-1.5">
<ProgressRing progress={file.progress} size={16} strokeWidth={2} color={color} />
<span className="font-mono text-[10px] font-medium" style={{ color }}>
{Math.round(file.progress)}%
</span>
</div>
);
}
if (file.status === 'done') {
return (
<div className="flex items-center gap-1.5">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" className="text-mint flex-shrink-0">
<circle cx="12" cy="12" r="10" fill="rgba(52,211,153,0.12)" stroke="currentColor" strokeWidth="1.5" />
<path d="M8 12l3 3 5-5" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
</svg>
<button
className="flex items-center justify-center w-6 h-6 rounded-md bg-transparent border-none cursor-pointer text-text-light/50 active:text-purple transition-all"
onClick={() => onPreview(file)}
title="Preview"
>
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" />
<circle cx="12" cy="12" r="3" />
</svg>
</button>
<button
className="inline-flex items-center gap-1 px-2 py-1 text-[10px] font-bold text-white bg-mint border-none rounded-md cursor-pointer shadow-[0_1px_4px_rgba(52,211,153,0.2)] transition-all"
onClick={() => onDownload(file)}
title="Download"
>
<svg width="9" height="9" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
<path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4M7 10l5 5 5-5M12 15V3" />
</svg>
.{file.targetFormat}
</button>
</div>
);
}
if (file.status === 'error') {
return (
<div className="flex items-center gap-1">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" className="text-red-400 flex-shrink-0">
<circle cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="1.5" fill="rgba(244,63,94,0.08)" />
<path d="M12 8v4M12 16h.01" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
</svg>
<span className="text-[10px] text-red-400 font-mono font-medium">Failed</span>
</div>
);
}
// No formats available
return (
<span className="text-[10px] text-text-light/40 italic">unsupported</span>
);
}