diff --git a/src/app/convert/page.tsx b/src/app/convert/page.tsx
new file mode 100644
index 0000000..8882e5c
--- /dev/null
+++ b/src/app/convert/page.tsx
@@ -0,0 +1,171 @@
+'use client';
+
+import { AnimatePresence, motion } from 'framer-motion';
+import Link from 'next/link';
+import { DropZone } from '@/components/DropZone';
+import { FileCard } from '@/components/FileCard';
+import { useFileUpload } from '@/hooks/useFileUpload';
+import { useConversion } from '@/hooks/useConversion';
+import { formatFileSize } from '@/lib/utils';
+
+export default function ConvertPage() {
+ const {
+ files,
+ isDragging,
+ inputRef,
+ handleDragEnter,
+ handleDragLeave,
+ handleDragOver,
+ handleDrop,
+ handleFileInput,
+ openFilePicker,
+ removeFile,
+ updateFile,
+ setTargetFormat,
+ clearAll,
+ } = useFileUpload();
+
+ const {
+ isConverting,
+ convertAll,
+ downloadFile,
+ downloadAllAsZip,
+ } = useConversion(updateFile);
+
+ const hasFiles = files.length > 0;
+ const convertableCount = files.filter(
+ (f) => f.targetFormat && f.status !== 'done' && f.availableFormats.length > 0
+ ).length;
+ const completedCount = files.filter((f) => f.status === 'done').length;
+ const totalSize = files.reduce((acc, f) => acc + f.size, 0);
+
+ return (
+
+ {/* Atmospheric backgrounds */}
+
+
+
+ {/* Header */}
+
+
+
+ T
+
+ Transmute
+
+
+ v1.0 / client-side
+
+
+
+ {/* Drop Zone */}
+
+
+ {/* File Grid */}
+ {hasFiles && (
+
+
+ {files.map((file, index) => (
+
+ ))}
+
+
+ )}
+
+ {/* Action Bar */}
+ {hasFiles && (
+
+
+
+ {files.length} file{files.length !== 1 ? 's' : ''}
+
+
+
+ {formatFileSize(totalSize)}
+
+ {completedCount > 0 && (
+ <>
+
+
+ {completedCount} converted
+
+ >
+ )}
+
+
+
+
+ Clear all
+
+
+ {completedCount > 0 && (
+
downloadAllAsZip(files)}
+ initial={{ opacity: 0, scale: 0.9 }}
+ animate={{ opacity: 1, scale: 1 }}
+ >
+
+
+
+ Download ZIP
+
+ )}
+
+
convertAll(files)}
+ disabled={isConverting || convertableCount === 0}
+ >
+ {isConverting ? (
+ <>
+
+
+
+ Converting...
+ >
+ ) : (
+ <>
+
+
+
+ Transmute {convertableCount > 0 ? `(${convertableCount})` : ''}
+ >
+ )}
+
+
+
+ )}
+
+ );
+}
diff --git a/src/app/globals.css b/src/app/globals.css
index ee1dadd..3bfab9e 100644
--- a/src/app/globals.css
+++ b/src/app/globals.css
@@ -1,730 +1,105 @@
@import "tailwindcss";
/* ============================================
- TRANSMUTE — Dark Industrial Theme
+ TRANSMUTE — Playful Pastel Theme
+ Force light color-scheme so dark mode OS
+ doesn't override our cream backgrounds.
============================================ */
-:root {
- --bg-primary: #09090b;
- --bg-secondary: #111114;
- --bg-tertiary: #18181c;
- --bg-card: rgba(255, 255, 255, 0.025);
- --bg-card-hover: rgba(255, 255, 255, 0.05);
-
- --border: rgba(255, 255, 255, 0.07);
- --border-hover: rgba(255, 255, 255, 0.14);
-
- --text-primary: #f0f0f2;
- --text-secondary: #7a7a85;
- --text-muted: #45454d;
-
- --accent: #00f0ff;
- --accent-glow: rgba(0, 240, 255, 0.15);
- --accent-dim: rgba(0, 240, 255, 0.5);
-
- --cat-image: #10b981;
- --cat-document: #0ea5e9;
- --cat-audio: #8b5cf6;
- --cat-video: #f43f5e;
- --cat-data: #f59e0b;
-
- --radius-sm: 6px;
- --radius-md: 10px;
- --radius-lg: 16px;
- --radius-xl: 20px;
-}
-
@theme inline {
- --color-background: var(--bg-primary);
- --color-foreground: var(--text-primary);
- --font-sans: var(--font-space-grotesk);
+ --color-bg-cream: #fef7f0;
+ --color-bg-warm: #fff5eb;
+ --color-bg-peach: #ffecd9;
+ --color-text-dark: #2d1f14;
+ --color-text-mid: #7a6552;
+ --color-text-light: #b8a08a;
+ --color-border-soft: rgba(180, 140, 100, 0.12);
+ --color-border-med: rgba(180, 140, 100, 0.25);
+ --color-pink: #f472b6;
+ --color-purple: #a78bfa;
+ --color-blue: #60a5fa;
+ --color-mint: #34d399;
+ --color-orange: #fb923c;
+ --color-teal: #2dd4bf;
+
+ --font-sans: var(--font-plus-jakarta-sans);
+ --font-serif: var(--font-fraunces);
--font-mono: var(--font-jetbrains-mono);
}
-/* ---- Base ---- */
-* {
- box-sizing: border-box;
+html {
+ color-scheme: light;
}
body {
- background: var(--bg-primary);
- color: var(--text-primary);
- font-family: var(--font-space-grotesk), system-ui, sans-serif;
+ background: var(--color-bg-cream);
+ color: var(--color-text-dark);
+ font-family: var(--font-plus-jakarta-sans), system-ui, sans-serif;
overflow-x: hidden;
}
-/* Noise texture overlay */
-.noise-overlay {
- position: fixed;
- inset: 0;
- pointer-events: none;
- z-index: 0;
- opacity: 0.035;
- background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
- background-repeat: repeat;
- background-size: 256px 256px;
-}
-
-/* Gradient mesh background */
-.bg-mesh {
- position: fixed;
- inset: 0;
- pointer-events: none;
- z-index: 0;
- background:
- radial-gradient(ellipse 60% 50% at 20% 0%, rgba(0, 240, 255, 0.04) 0%, transparent 60%),
- radial-gradient(ellipse 50% 40% at 80% 100%, rgba(139, 92, 246, 0.03) 0%, transparent 60%);
-}
-
/* ---- Scrollbar ---- */
-::-webkit-scrollbar { width: 6px; }
+::-webkit-scrollbar { width: 8px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb {
- background: rgba(255, 255, 255, 0.1);
- border-radius: 3px;
+ background: rgba(180, 140, 100, 0.15);
+ border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
- background: rgba(255, 255, 255, 0.18);
+ background: rgba(180, 140, 100, 0.25);
}
-/* ============================================
- Header
- ============================================ */
-.app-header {
- position: sticky;
- top: 0;
- z-index: 50;
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: 16px 24px;
- background: rgba(9, 9, 11, 0.8);
- backdrop-filter: blur(12px);
- border-bottom: 1px solid var(--border);
+/* ---- Gradient text utility ---- */
+/* Removed — using solid text colors instead */
+
+/* ---- Animations ---- */
+@keyframes badge-pulse {
+ 0%, 100% { opacity: 1; transform: scale(1); }
+ 50% { opacity: 0.6; transform: scale(0.85); }
}
-.logo {
- display: flex;
- align-items: center;
- gap: 10px;
- font-family: var(--font-space-grotesk), sans-serif;
- font-weight: 700;
- font-size: 18px;
- letter-spacing: -0.02em;
- color: var(--text-primary);
+@keyframes pulse-soft {
+ 0%, 100% { box-shadow: 0 4px 20px rgba(244, 114, 182, 0.25); }
+ 50% { box-shadow: 0 4px 32px rgba(167, 139, 250, 0.35); }
}
-.logo-icon {
- width: 28px;
- height: 28px;
- border-radius: 7px;
- background: linear-gradient(135deg, var(--accent), #0090ff);
- display: flex;
- align-items: center;
- justify-content: center;
- color: #000;
- font-weight: 800;
- font-size: 14px;
+.animate-badge-pulse {
+ animation: badge-pulse 2s ease-in-out infinite;
}
-.header-tag {
- font-family: var(--font-jetbrains-mono), monospace;
- font-size: 11px;
- color: var(--text-muted);
- padding: 3px 8px;
- border: 1px solid var(--border);
- border-radius: 20px;
- letter-spacing: 0.03em;
+.animate-pulse-soft {
+ animation: pulse-soft 2s ease-in-out infinite;
}
-/* ============================================
- Drop Zone — Hero (no files)
- ============================================ */
-.drop-zone-hero {
- position: relative;
- display: flex;
- align-items: center;
- justify-content: center;
- min-height: calc(100vh - 57px);
- padding: 40px 24px;
+/* ---- Dot pattern background ---- */
+.bg-dots {
+ background-image: radial-gradient(circle, rgba(180, 140, 100, 0.12) 1px, transparent 1px);
+ background-size: 24px 24px;
}
-.drop-zone-inner {
- position: relative;
- width: 100%;
- max-width: 640px;
- padding: 64px 40px;
- border: 1.5px dashed var(--border-hover);
- border-radius: var(--radius-xl);
- background: var(--bg-card);
- transition: all 0.4s cubic-bezier(0.16, 1, 0.3, 1);
- cursor: pointer;
+/* ---- Pastel mesh background ---- */
+.bg-pastel-mesh {
+ background-color: rgba(244, 114, 182, 0.03);
}
-.drop-zone-inner.dragging {
- border-color: var(--accent);
- background: var(--accent-glow);
- box-shadow:
- 0 0 40px rgba(0, 240, 255, 0.08),
- inset 0 0 40px rgba(0, 240, 255, 0.03);
-}
-
-/* Corner accents */
-.corner-accent {
- position: absolute;
- width: 16px;
- height: 16px;
- border-color: var(--accent-dim);
- transition: border-color 0.3s;
-}
-
-.drop-zone-inner.dragging .corner-accent {
- border-color: var(--accent);
-}
-
-.corner-accent.top-left {
- top: -1px; left: -1px;
- border-top: 2px solid;
- border-left: 2px solid;
- border-top-left-radius: var(--radius-xl);
-}
-.corner-accent.top-right {
- top: -1px; right: -1px;
- border-top: 2px solid;
- border-right: 2px solid;
- border-top-right-radius: var(--radius-xl);
-}
-.corner-accent.bottom-left {
- bottom: -1px; left: -1px;
- border-bottom: 2px solid;
- border-left: 2px solid;
- border-bottom-left-radius: var(--radius-xl);
-}
-.corner-accent.bottom-right {
- bottom: -1px; right: -1px;
- border-bottom: 2px solid;
- border-right: 2px solid;
- border-bottom-right-radius: var(--radius-xl);
-}
-
-.drop-zone-content {
- display: flex;
- flex-direction: column;
- align-items: center;
- text-align: center;
- gap: 16px;
-}
-
-.upload-icon {
- color: var(--accent);
- margin-bottom: 8px;
- filter: drop-shadow(0 0 12px rgba(0, 240, 255, 0.3));
-}
-
-.drop-zone-title {
- font-family: var(--font-space-grotesk), sans-serif;
- font-size: 28px;
- font-weight: 700;
- letter-spacing: -0.03em;
- color: var(--text-primary);
- line-height: 1.2;
-}
-
-.drop-zone-subtitle {
- font-size: 15px;
- color: var(--text-secondary);
- max-width: 380px;
- line-height: 1.5;
-}
-
-.browse-button {
- display: inline-flex;
- align-items: center;
- gap: 8px;
- padding: 10px 24px;
- margin-top: 8px;
- font-family: var(--font-space-grotesk), sans-serif;
- font-size: 14px;
- font-weight: 600;
- color: #000;
- background: var(--accent);
- border: none;
- border-radius: var(--radius-md);
- cursor: pointer;
- transition: all 0.2s;
- box-shadow: 0 0 20px rgba(0, 240, 255, 0.2);
-}
-
-.browse-button:hover {
- box-shadow: 0 0 30px rgba(0, 240, 255, 0.3);
-}
-
-.drop-zone-hint {
- font-family: var(--font-jetbrains-mono), monospace;
- font-size: 11px;
- color: var(--text-muted);
- margin-top: 8px;
- letter-spacing: 0.02em;
-}
-
-/* ============================================
- Drop Zone — Compact (has files)
- ============================================ */
-.drop-zone-compact {
- padding: 16px 24px;
- position: relative;
- z-index: 1;
-}
-
-.compact-drop-area {
- display: flex;
- align-items: center;
- justify-content: center;
- gap: 10px;
- padding: 14px;
- border: 1.5px dashed var(--border);
- border-radius: var(--radius-md);
- background: var(--bg-card);
- cursor: pointer;
- transition: all 0.3s;
- color: var(--text-secondary);
- font-size: 13px;
- font-weight: 500;
-}
-
-.compact-drop-area:hover {
- border-color: var(--border-hover);
- background: var(--bg-card-hover);
- color: var(--text-primary);
-}
-
-.compact-drop-area.dragging {
- border-color: var(--accent);
- background: var(--accent-glow);
- color: var(--accent);
-}
-
-/* ============================================
- File Cards
- ============================================ */
-.file-grid {
- display: grid;
- grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
- gap: 16px;
- padding: 0 24px 120px;
- position: relative;
- z-index: 1;
-}
-
-.file-card {
- position: relative;
- background: var(--bg-card);
- border: 1px solid var(--border);
- border-radius: var(--radius-lg);
- padding: 16px;
- display: flex;
- flex-direction: column;
- gap: 12px;
- transition: all 0.25s ease;
- overflow: hidden;
-}
-
-.file-card:hover {
- background: var(--bg-card-hover);
- border-color: var(--border-hover);
-}
-
-.card-accent-line {
+/* ---- Top-border accent for privacy card ---- */
+.border-t-rainbow::before {
+ content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
- height: 2px;
- opacity: 0.6;
+ height: 3px;
+ background: var(--color-pink);
+ border-radius: inherit;
}
-.card-header {
- display: flex;
- align-items: center;
- justify-content: space-between;
-}
-
-.category-badge {
- font-family: var(--font-jetbrains-mono), monospace;
- font-size: 10px;
- font-weight: 600;
- text-transform: uppercase;
- letter-spacing: 0.08em;
- padding: 3px 8px;
- border-radius: 20px;
- border: 1px solid;
-}
-
-.remove-btn {
- width: 24px;
- height: 24px;
- display: flex;
- align-items: center;
- justify-content: center;
- border: none;
- background: transparent;
- color: var(--text-muted);
- cursor: pointer;
- border-radius: 6px;
- transition: all 0.2s;
-}
-
-.remove-btn:hover {
- color: #f43f5e;
- background: rgba(244, 63, 94, 0.1);
-}
-
-/* Preview area */
-.card-preview {
- position: relative;
- width: 100%;
- height: 100px;
- border-radius: var(--radius-sm);
- background: var(--bg-secondary);
- overflow: hidden;
- display: flex;
- align-items: center;
- justify-content: center;
-}
-
-.preview-image {
- width: 100%;
- height: 100%;
- object-fit: cover;
-}
-
-.preview-icon {
- display: flex;
- flex-direction: column;
- align-items: center;
- gap: 4px;
-}
-
-.file-ext {
- font-family: var(--font-jetbrains-mono), monospace;
- font-size: 18px;
- font-weight: 700;
- opacity: 0.5;
-}
-
-.progress-overlay,
-.done-overlay,
-.error-overlay {
- position: absolute;
- inset: 0;
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- gap: 4px;
- background: rgba(9, 9, 11, 0.75);
- backdrop-filter: blur(4px);
-}
-
-.progress-text {
- font-family: var(--font-jetbrains-mono), monospace;
- font-size: 11px;
- color: var(--text-secondary);
- margin-top: 2px;
-}
-
-/* File info */
-.card-info {
- display: flex;
- flex-direction: column;
- gap: 2px;
-}
-
-.file-name {
- font-size: 13px;
- font-weight: 600;
- color: var(--text-primary);
- line-height: 1.3;
- word-break: break-word;
-}
-
-.file-size {
- font-family: var(--font-jetbrains-mono), monospace;
- font-size: 11px;
- color: var(--text-muted);
-}
-
-.error-message {
- font-size: 11px;
- color: #f43f5e;
- background: rgba(244, 63, 94, 0.08);
- padding: 6px 8px;
- border-radius: var(--radius-sm);
- line-height: 1.4;
-}
-
-/* Format selector */
-.format-selector {
- display: flex;
- align-items: center;
- gap: 8px;
-}
-
-.format-from {
- font-family: var(--font-jetbrains-mono), monospace;
- font-size: 12px;
- font-weight: 600;
- color: var(--text-secondary);
- padding: 4px 8px;
- background: var(--bg-secondary);
- border-radius: var(--radius-sm);
-}
-
-.format-arrow {
- color: var(--text-muted);
- flex-shrink: 0;
-}
-
-.format-select {
- flex: 1;
- font-family: var(--font-jetbrains-mono), monospace;
- font-size: 12px;
- font-weight: 600;
- color: var(--accent);
- background: var(--bg-secondary);
- border: 1px solid;
- border-radius: var(--radius-sm);
- padding: 4px 8px;
- cursor: pointer;
- outline: none;
+/* ---- Format select dropdown arrow ---- */
+.select-arrow-warm {
appearance: none;
-webkit-appearance: none;
- background-image: url("data:image/svg+xml,%3Csvg width='10' height='6' viewBox='0 0 10 6' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1 1L5 5L9 1' stroke='%237a7a85' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
+ background-image: url("data:image/svg+xml,%3Csvg width='10' height='6' viewBox='0 0 10 6' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1 1L5 5L9 1' stroke='%23b8a08a' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
background-repeat: no-repeat;
- background-position: right 8px center;
- padding-right: 24px;
-}
-
-.format-select option {
- background: var(--bg-secondary);
- color: var(--text-primary);
-}
-
-.download-btn {
- display: flex;
- align-items: center;
- justify-content: center;
- gap: 6px;
- width: 100%;
- padding: 8px;
- font-family: var(--font-space-grotesk), sans-serif;
- font-size: 12px;
- font-weight: 600;
- color: #000;
- background: #10b981;
- border: none;
- border-radius: var(--radius-sm);
- cursor: pointer;
- transition: all 0.2s;
-}
-
-.download-btn:hover {
- background: #34d399;
- box-shadow: 0 0 16px rgba(16, 185, 129, 0.3);
-}
-
-.unsupported-msg {
- font-size: 11px;
- color: var(--text-muted);
- text-align: center;
- padding: 6px;
- background: var(--bg-secondary);
- border-radius: var(--radius-sm);
-}
-
-/* ============================================
- Action Bar
- ============================================ */
-.action-bar {
- position: fixed;
- bottom: 0;
- left: 0;
- right: 0;
- z-index: 40;
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: 16px 24px;
- background: rgba(9, 9, 11, 0.85);
- backdrop-filter: blur(16px);
- border-top: 1px solid var(--border);
-}
-
-.action-bar-info {
- display: flex;
- align-items: center;
- gap: 16px;
-}
-
-.action-bar-stat {
- font-family: var(--font-jetbrains-mono), monospace;
- font-size: 12px;
- color: var(--text-secondary);
- display: flex;
- align-items: center;
- gap: 6px;
-}
-
-.action-bar-stat strong {
- color: var(--text-primary);
- font-weight: 600;
-}
-
-.stat-divider {
- width: 1px;
- height: 16px;
- background: var(--border);
-}
-
-.action-bar-buttons {
- display: flex;
- align-items: center;
- gap: 10px;
-}
-
-.btn-secondary {
- display: inline-flex;
- align-items: center;
- gap: 6px;
- padding: 8px 16px;
- font-family: var(--font-space-grotesk), sans-serif;
- font-size: 13px;
- font-weight: 600;
- color: var(--text-secondary);
- background: transparent;
- border: 1px solid var(--border);
- border-radius: var(--radius-md);
- cursor: pointer;
- transition: all 0.2s;
-}
-
-.btn-secondary:hover {
- color: var(--text-primary);
- border-color: var(--border-hover);
- background: var(--bg-card);
-}
-
-.btn-convert {
- display: inline-flex;
- align-items: center;
- gap: 8px;
- padding: 10px 28px;
- font-family: var(--font-space-grotesk), sans-serif;
- font-size: 14px;
- font-weight: 700;
- color: #000;
- background: var(--accent);
- border: none;
- border-radius: var(--radius-md);
- cursor: pointer;
- transition: all 0.2s;
- box-shadow: 0 0 24px rgba(0, 240, 255, 0.2);
- letter-spacing: -0.01em;
-}
-
-.btn-convert:hover:not(:disabled) {
- box-shadow: 0 0 36px rgba(0, 240, 255, 0.35);
- transform: translateY(-1px);
-}
-
-.btn-convert:disabled {
- opacity: 0.5;
- cursor: not-allowed;
- box-shadow: none;
-}
-
-.btn-convert.converting {
- background: var(--accent-dim);
- animation: pulse-glow 2s ease-in-out infinite;
-}
-
-@keyframes pulse-glow {
- 0%, 100% { box-shadow: 0 0 24px rgba(0, 240, 255, 0.2); }
- 50% { box-shadow: 0 0 40px rgba(0, 240, 255, 0.4); }
-}
-
-.btn-download-all {
- display: inline-flex;
- align-items: center;
- gap: 6px;
- padding: 10px 20px;
- font-family: var(--font-space-grotesk), sans-serif;
- font-size: 13px;
- font-weight: 600;
- color: #000;
- background: #10b981;
- border: none;
- border-radius: var(--radius-md);
- cursor: pointer;
- transition: all 0.2s;
- box-shadow: 0 0 16px rgba(16, 185, 129, 0.2);
-}
-
-.btn-download-all:hover {
- background: #34d399;
- box-shadow: 0 0 24px rgba(16, 185, 129, 0.35);
-}
-
-/* ============================================
- Responsive
- ============================================ */
-@media (max-width: 640px) {
- .file-grid {
- grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
- gap: 12px;
- padding: 0 16px 120px;
- }
-
- .drop-zone-hero {
- padding: 24px 16px;
- min-height: calc(100vh - 57px);
- }
-
- .drop-zone-inner {
- padding: 48px 24px;
- }
-
- .drop-zone-title {
- font-size: 22px;
- }
-
- .action-bar {
- flex-direction: column;
- gap: 12px;
- padding: 12px 16px;
- }
-
- .action-bar-buttons {
- width: 100%;
- }
-
- .btn-convert {
- flex: 1;
- }
-
- .app-header {
- padding: 12px 16px;
- }
-}
-
-/* ============================================
- Utility animations
- ============================================ */
-@keyframes fade-in {
- from { opacity: 0; }
- to { opacity: 1; }
-}
-
-.animate-fade-in {
- animation: fade-in 0.5s ease both;
+ background-position: right 10px center;
+ padding-right: 28px;
}
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index 6c026ba..2cd88af 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -1,11 +1,17 @@
import type { Metadata } from "next";
-import { Space_Grotesk, JetBrains_Mono } from "next/font/google";
+import { Fraunces, Plus_Jakarta_Sans, JetBrains_Mono } from "next/font/google";
import "./globals.css";
-const spaceGrotesk = Space_Grotesk({
- variable: "--font-space-grotesk",
+const fraunces = Fraunces({
+ variable: "--font-fraunces",
subsets: ["latin"],
- weight: ["400", "500", "600", "700"],
+ weight: ["400", "500", "600", "700", "800", "900"],
+});
+
+const plusJakartaSans = Plus_Jakarta_Sans({
+ variable: "--font-plus-jakarta-sans",
+ subsets: ["latin"],
+ weight: ["400", "500", "600", "700", "800"],
});
const jetbrainsMono = JetBrains_Mono({
@@ -40,7 +46,7 @@ export default function RootLayout({
return (
{children}
diff --git a/src/app/page.tsx b/src/app/page.tsx
index 0fc3c89..4c4785e 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -1,167 +1,364 @@
'use client';
-import { AnimatePresence, motion } from 'framer-motion';
-import { DropZone } from '@/components/DropZone';
-import { FileCard } from '@/components/FileCard';
-import { useFileUpload } from '@/hooks/useFileUpload';
-import { useConversion } from '@/hooks/useConversion';
-import { formatFileSize } from '@/lib/utils';
+import { motion } from 'framer-motion';
+import Link from 'next/link';
-export default function Home() {
- const {
- files,
- isDragging,
- inputRef,
- handleDragEnter,
- handleDragLeave,
- handleDragOver,
- handleDrop,
- handleFileInput,
- openFilePicker,
- removeFile,
- updateFile,
- setTargetFormat,
- clearAll,
- } = useFileUpload();
+const floatingBadges = [
+ { label: 'PNG', color: 'bg-pink-200', dot: 'bg-pink-400', top: '18%', left: '8%', delay: 0 },
+ { label: 'MP4', color: 'bg-orange-100', dot: 'bg-orange-400', top: '22%', right: '10%', delay: 0.5 },
+ { label: 'CSV', color: 'bg-emerald-100', dot: 'bg-emerald-400', bottom: '32%', left: '6%', delay: 1.0 },
+ { label: 'PDF', color: 'bg-blue-100', dot: 'bg-blue-400', bottom: '28%', right: '8%', delay: 0.3 },
+ { label: 'WAV', color: 'bg-purple-100', dot: 'bg-purple-400', top: '42%', left: '3%', delay: 0.7 },
+ { label: 'WEBP', color: 'bg-pink-100', dot: 'bg-pink-400', top: '35%', right: '4%', delay: 1.2 },
+];
- const {
- isConverting,
- convertAll,
- downloadFile,
- downloadAllAsZip,
- } = useConversion(updateFile);
+const features = [
+ {
+ icon: '\u{1F5BC}',
+ title: 'Images',
+ desc: 'PNG, JPG, WebP, GIF, BMP, AVIF, SVG \u2014 convert between any format using Canvas API.',
+ bg: 'bg-pink-50',
+ iconBg: 'bg-pink-100',
+ formats: ['PNG', 'JPG', 'WebP', 'GIF', 'AVIF', 'SVG'],
+ wide: true,
+ },
+ {
+ icon: '\u{1F4C4}',
+ title: 'Documents',
+ desc: 'DOCX, PDF, Markdown, HTML, TXT \u2014 preserves formatting with styled rendering.',
+ bg: 'bg-blue-50',
+ iconBg: 'bg-blue-100',
+ formats: ['DOCX', 'PDF', 'MD', 'HTML', 'TXT'],
+ wide: false,
+ },
+ {
+ icon: '\u{1F3B5}',
+ title: 'Audio',
+ desc: 'MP3, WAV, OGG, AAC, FLAC, M4A \u2014 powered by FFmpeg WebAssembly.',
+ bg: 'bg-purple-50',
+ iconBg: 'bg-purple-100',
+ formats: ['MP3', 'WAV', 'OGG', 'FLAC'],
+ wide: false,
+ },
+ {
+ icon: '\u{1F3AC}',
+ title: 'Video',
+ desc: 'MP4, WebM, AVI, MOV, MKV \u2014 full video transcoding in your browser.',
+ bg: 'bg-orange-50',
+ iconBg: 'bg-orange-100',
+ formats: ['MP4', 'WebM', 'AVI', 'MOV'],
+ wide: false,
+ },
+ {
+ icon: '\u{1F4CA}',
+ title: 'Data',
+ desc: 'CSV, JSON, XML, YAML, TSV \u2014 smart parsing with structure preservation.',
+ bg: 'bg-emerald-50',
+ iconBg: 'bg-emerald-100',
+ formats: ['CSV', 'JSON', 'XML', 'YAML', 'TSV'],
+ wide: true,
+ },
+];
- const hasFiles = files.length > 0;
- const convertableCount = files.filter(
- (f) => f.targetFormat && f.status !== 'done' && f.availableFormats.length > 0
- ).length;
- const completedCount = files.filter((f) => f.status === 'done').length;
- const totalSize = files.reduce((acc, f) => acc + f.size, 0);
+const stagger = {
+ hidden: {},
+ visible: { transition: { staggerChildren: 0.08 } },
+};
+const fadeUp = {
+ hidden: { opacity: 0, y: 24 },
+ visible: {
+ opacity: 1,
+ y: 0,
+ transition: { duration: 0.6, ease: [0.16, 1, 0.3, 1] as const },
+ },
+};
+
+export default function LandingPage() {
return (
-
+
{/* Atmospheric backgrounds */}
-
-
+
+
- {/* Header */}
-
-
- v1.0 / client-side
+ {/* ──── NAV ──── */}
+
+
+
+ T
+
+ Transmute
+
+
+ Open Converter
+
- {/* Drop Zone */}
-
+ {/* ──── HERO ──── */}
+
+ {/* Floating format badges */}
+ {floatingBadges.map((badge) => (
+
+
+
+ .{badge.label}
+
+
+ ))}
- {/* File Grid */}
- {hasFiles && (
-
- {files.map((file, index) => (
-
- ))}
-
-
- )}
+ {/* Badge */}
+
+
+ 100% client-side, forever free
+
- {/* Action Bar */}
- {hasFiles && (
-
-
-
- {files.length} file{files.length !== 1 ? 's' : ''}
-
-
-
- {formatFileSize(totalSize)}
-
- {completedCount > 0 && (
- <>
-
-
- {completedCount} converted
-
- >
- )}
-
+ {/* Title */}
+
+ Convert anything to{' '}
+ everything
+
-
-
- Clear all
-
+ {/* Subtitle */}
+
+ Drop images, docs, audio, video, or data files — get instant conversions
+ right in your browser. No uploads. No accounts. No limits.
+
- {completedCount > 0 && (
-
downloadAllAsZip(files)}
- initial={{ opacity: 0, scale: 0.9 }}
- animate={{ opacity: 1, scale: 1 }}
- whileHover={{ scale: 1.03 }}
- whileTap={{ scale: 0.97 }}
- >
-
-
-
- Download ZIP
-
- )}
-
-
convertAll(files)}
- disabled={isConverting || convertableCount === 0}
- whileHover={!isConverting && convertableCount > 0 ? { scale: 1.03 } : {}}
- whileTap={!isConverting && convertableCount > 0 ? { scale: 0.97 } : {}}
+ {/* CTAs */}
+
+
- {isConverting ? (
- <>
-
-
+
+
+
+ Start Converting
+
+
+ See what's possible
+
+
+
+
+
+
+
+
+ {/* ──── FEATURES ──── */}
+
+
+
+ Formats
+
+
+ Every format you need
+
+
+ 40+ file formats across 5 categories, all converted instantly with zero quality loss.
+
+
+
+
+ {features.map((feat) => (
+
+
+ {feat.icon}
+
+ {feat.title}
+ {feat.desc}
+
+ {feat.formats.map((f) => (
+
+ .{f}
+
+ ))}
+
+
+ ))}
+
+
+
+ {/* ──── HOW IT WORKS ──── */}
+
+
+
+ How it works
+
+
+ Three steps. That's it.
+
+
+
+
+ {[
+ { num: '1', title: 'Drop your files', desc: 'Drag and drop any file \u2014 or click to browse. We accept everything.' },
+ { num: '2', title: 'Pick a format', desc: 'Choose your target format from smart suggestions based on file type.' },
+ { num: '3', title: 'Download', desc: 'Hit convert and download instantly. Files never leave your browser.' },
+ ].map((step, i) => (
+
+
+ {step.num}
+
+ {step.title}
+ {step.desc}
+ {i < 2 && (
+
+
+
- Converting...
- >
- ) : (
- <>
-
-
-
- Transmute {convertableCount > 0 ? `(${convertableCount})` : ''}
- >
+
)}
-
+
+ ))}
+
+
+
+ {/* ──── PRIVACY ──── */}
+
+
+
+ Your files stay yours
+
+ Every conversion happens entirely in your browser using WebAssembly and Canvas APIs.
+ No file ever touches a server. No data is collected. No account needed. Ever.
+
+
+ {[
+ { label: 'No uploads', color: 'bg-emerald-50', stroke: '#34d399' },
+ { label: 'No servers', color: 'bg-blue-50', stroke: '#60a5fa' },
+ { label: 'No tracking', color: 'bg-purple-50', stroke: '#a78bfa' },
+ { label: 'No limits', color: 'bg-orange-50', stroke: '#fb923c' },
+ ].map((b) => (
+
+ ))}
- )}
+
+
+ {/* ──── FOOTER CTA ──── */}
+
+
+ Ready to transmute?
+
+
+
+
+
+
+ Open Converter
+
+
+
+
+ {/* ──── FOOTER ──── */}
+
);
}
diff --git a/src/components/DropZone.tsx b/src/components/DropZone.tsx
index 9d2dcd6..154d1e0 100644
--- a/src/components/DropZone.tsx
+++ b/src/components/DropZone.tsx
@@ -26,10 +26,11 @@ export function DropZone({
onFileInput,
onBrowse,
}: DropZoneProps) {
+ // Compact mode — thin bar when files are present
if (hasFiles) {
return (
-
+
- Drop more files or click to browse
+
+ {isDragging ? 'Release to add files' : 'Drop more files or click to browse'}
+
);
}
+ // Hero mode — full drop zone when no files
return (
- {/* Animated corner accents */}
-
-
-
-
+ {/* Corner accents */}
+
+
+
+
{/* Upload icon */}
-
+
-
+
{isDragging ? 'Release to transmute' : 'Drop anything.'}
-
+
{isDragging
? 'Your files are ready for transformation'
: 'Images, documents, audio, video, data \u2014 all formats welcome'}
-
- 100% client-side \u2014 your files never leave your browser
+
+ 100% client-side — your files never leave your browser
diff --git a/src/components/FileCard.tsx b/src/components/FileCard.tsx
index ed44acb..c8044e7 100644
--- a/src/components/FileCard.tsx
+++ b/src/components/FileCard.tsx
@@ -25,30 +25,27 @@ export function FileCard({
return (
{/* Top accent line */}
{/* Header: category badge + remove */}
-
+
onRemove(file.id)}
aria-label="Remove file"
>
@@ -72,80 +69,94 @@ export function FileCard({
{/* File preview / icon */}
-
+
{file.preview ? (
/* eslint-disable-next-line @next/next/no-img-element */
) : (
-
-
.{file.extension}
+
+
+ .{file.extension}
+
)}
{/* Progress overlay */}
{file.status === 'converting' && (
-
+
-
{Math.round(file.progress)}%
+
+ {Math.round(file.progress)}%
+
)}
{/* Done overlay */}
{file.status === 'done' && (
-
-
-
+
)}
{/* Error overlay */}
{file.status === 'error' && (
-
{/* File info */}
-
-
+
+
{truncateFilename(file.name)}
-
{formatFileSize(file.size)}
+
+ {formatFileSize(file.size)}
+
{/* Error message */}
{file.status === 'error' && file.error && (
-
{file.error}
+
+ {file.error}
+
)}
{/* Format selector */}
{file.availableFormats.length > 0 && file.status !== 'done' && (
-
-
.{file.extension}
-
+
+
+ .{file.extension}
+
+
onSetFormat(file.id, e.target.value)}
- className="format-select"
+ className="select-arrow-warm flex-1 min-w-0 font-mono text-xs font-bold text-text-dark bg-white px-3 py-1.5 rounded-xl border border-border-soft cursor-pointer hover:border-border-med focus:outline-none focus:ring-2 focus:ring-pink/20 focus:border-pink/40 transition-all"
style={{ borderColor: `${categoryColor}40` }}
>
{file.availableFormats.map((fmt) => (
@@ -159,24 +170,26 @@ export function FileCard({
{/* Download button */}
{file.status === 'done' && (
- onDownload(file)}
- initial={{ opacity: 0, y: 5 }}
- animate={{ opacity: 1, y: 0 }}
- whileHover={{ scale: 1.02 }}
- whileTap={{ scale: 0.98 }}
- >
-
-
-
- Download .{file.targetFormat}
-
+
+
onDownload(file)}
+ initial={{ opacity: 0, y: 5 }}
+ animate={{ opacity: 1, y: 0 }}
+ whileHover={{ scale: 1.02 }}
+ whileTap={{ scale: 0.98 }}
+ >
+
+
+
+ Download .{file.targetFormat}
+
+
)}
{/* Unsupported message */}
{file.availableFormats.length === 0 && (
-
+
Format not supported for conversion
)}
diff --git a/src/components/ProgressRing.tsx b/src/components/ProgressRing.tsx
index 66b447b..5b7391e 100644
--- a/src/components/ProgressRing.tsx
+++ b/src/components/ProgressRing.tsx
@@ -11,7 +11,7 @@ export function ProgressRing({
progress,
size = 36,
strokeWidth = 3,
- color = '#00F0FF',
+ color = '#f472b6',
}: ProgressRingProps) {
const radius = (size - strokeWidth) / 2;
const circumference = 2 * Math.PI * radius;
@@ -30,7 +30,7 @@ export function ProgressRing({
cy={size / 2}
r={radius}
fill="none"
- stroke="rgba(255,255,255,0.08)"
+ stroke="rgba(0,0,0,0.06)"
strokeWidth={strokeWidth}
/>
{/* Progress ring */}
diff --git a/src/types/index.ts b/src/types/index.ts
index 9c48709..7ac56d3 100644
--- a/src/types/index.ts
+++ b/src/types/index.ts
@@ -26,12 +26,12 @@ export interface ConversionResult {
}
export const CATEGORY_COLORS: Record = {
- image: '#10b981',
- document: '#0ea5e9',
- audio: '#8b5cf6',
- video: '#f43f5e',
- data: '#f59e0b',
- unknown: '#6b7280',
+ image: '#f472b6', // soft pink
+ document: '#60a5fa', // soft blue
+ audio: '#a78bfa', // soft purple
+ video: '#fb923c', // soft orange
+ data: '#34d399', // soft mint
+ unknown: '#94a3b8', // soft slate
};
export const CATEGORY_ICONS: Record = {