Spaces:
Running
Running
| <html lang="en" class="scroll-smooth"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>SecureCrypt | Military-Grade Client-Side Encryption</title> | |
| <meta name="description" content="Secure client-side encryption for text and files. Zero server storage, AES-256-GCM encryption."> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <script src="https://unpkg.com/lucide@latest"></script> | |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet"> | |
| <script> | |
| tailwind.config = { | |
| darkMode: 'class', | |
| theme: { | |
| extend: { | |
| fontFamily: { | |
| sans: ['Inter', 'sans-serif'], | |
| mono: ['JetBrains Mono', 'monospace'], | |
| }, | |
| colors: { | |
| primary: { | |
| 50: '#eff6ff', | |
| 100: '#dbeafe', | |
| 500: '#3b82f6', | |
| 600: '#2563eb', | |
| 700: '#1d4ed8', | |
| 900: '#1e3a8a', | |
| }, | |
| crypto: { | |
| encrypt: '#10b981', | |
| decrypt: '#f59e0b', | |
| danger: '#ef4444', | |
| } | |
| }, | |
| animation: { | |
| 'pulse-slow': 'pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite', | |
| 'float': 'float 6s ease-in-out infinite', | |
| 'shimmer': 'shimmer 2s linear infinite', | |
| }, | |
| keyframes: { | |
| float: { | |
| '0%, 100%': { transform: 'translateY(0)' }, | |
| '50%': { transform: 'translateY(-10px)' }, | |
| }, | |
| shimmer: { | |
| '0%': { backgroundPosition: '-1000px 0' }, | |
| '100%': { backgroundPosition: '1000px 0' }, | |
| } | |
| } | |
| } | |
| } | |
| } | |
| </script> | |
| <style> | |
| .glass-panel { | |
| background: rgba(255, 255, 255, 0.05); | |
| backdrop-filter: blur(10px); | |
| border: 1px solid rgba(255, 255, 255, 0.1); | |
| } | |
| .dark .glass-panel { | |
| background: rgba(17, 24, 39, 0.7); | |
| border: 1px solid rgba(255, 255, 255, 0.05); | |
| } | |
| .gradient-text { | |
| background: linear-gradient(135deg, #3b82f6 0%, #8b5cf6 100%); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| background-clip: text; | |
| } | |
| .security-grid { | |
| background-image: radial-gradient(circle at 1px 1px, rgba(59, 130, 246, 0.15) 1px, transparent 0); | |
| background-size: 20px 20px; | |
| } | |
| .drop-zone { | |
| transition: all 0.3s ease; | |
| } | |
| .drop-zone.drag-over { | |
| border-color: #3b82f6; | |
| background: rgba(59, 130, 246, 0.1); | |
| transform: scale(1.02); | |
| } | |
| .tab-active { | |
| background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%); | |
| color: white; | |
| } | |
| .strength-weak { background: #ef4444; } | |
| .strength-fair { background: #f59e0b; } | |
| .strength-good { background: #3b82f6; } | |
| .strength-strong { background: #10b981; } | |
| .toast { | |
| animation: slideIn 0.3s ease-out; | |
| } | |
| @keyframes slideIn { | |
| from { transform: translateX(100%); opacity: 0; } | |
| to { transform: translateX(0); opacity: 1; } | |
| } | |
| .encrypt-mode { border-color: #10b981; } | |
| .decrypt-mode { border-color: #f59e0b; } | |
| </style> | |
| </head> | |
| <body class="bg-gray-50 dark:bg-gray-900 text-gray-900 dark:text-gray-100 transition-colors duration-300 font-sans"> | |
| <!-- Navigation --> | |
| <nav class="fixed w-full z-50 glass-panel border-b border-gray-200 dark:border-gray-800"> | |
| <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> | |
| <div class="flex justify-between h-16 items-center"> | |
| <div class="flex items-center space-x-3"> | |
| <div class="w-10 h-10 bg-gradient-to-br from-blue-500 to-purple-600 rounded-xl flex items-center justify-center shadow-lg"> | |
| <i data-lucide="shield-check" class="w-6 h-6 text-white"></i> | |
| </div> | |
| <span class="text-xl font-bold bg-gradient-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent"> | |
| SecureCrypt | |
| </span> | |
| </div> | |
| <div class="flex items-center space-x-4"> | |
| <button onclick="toggleTheme()" class="p-2 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-800 transition-colors"> | |
| <i data-lucide="sun" class="w-5 h-5 hidden dark:block"></i> | |
| <i data-lucide="moon" class="w-5 h-5 block dark:hidden"></i> | |
| </button> | |
| <a href="#security" class="hidden md:block text-sm font-medium hover:text-blue-600 dark:hover:text-blue-400 transition-colors"> | |
| Security Specs | |
| </a> | |
| <div class="flex items-center space-x-2 px-3 py-1 rounded-full bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-400 text-xs font-semibold"> | |
| <span class="w-2 h-2 bg-green-500 rounded-full animate-pulse"></span> | |
| <span>Client-Side Only</span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </nav> | |
| <!-- Hero Section --> | |
| <section class="pt-32 pb-16 px-4 sm:px-6 lg:px-8 relative overflow-hidden"> | |
| <div class="absolute inset-0 security-grid opacity-50"></div> | |
| <div class="absolute top-0 left-1/2 -translate-x-1/2 w-full h-96 bg-gradient-to-b from-blue-500/10 to-transparent pointer-events-none"></div> | |
| <div class="max-w-4xl mx-auto text-center relative z-10"> | |
| <div class="inline-flex items-center space-x-2 px-4 py-2 rounded-full bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 text-sm font-medium mb-6 animate-float"> | |
| <i data-lucide="lock" class="w-4 h-4"></i> | |
| <span>Military-Grade AES-256-GCM Encryption</span> | |
| </div> | |
| <h1 class="text-5xl md:text-6xl font-bold mb-6 leading-tight"> | |
| Encrypt Your Data <br> | |
| <span class="gradient-text">In the Browser</span> | |
| </h1> | |
| <p class="text-xl text-gray-600 dark:text-gray-400 mb-8 max-w-2xl mx-auto"> | |
| Zero server storage. Zero data transmission. Your files and text never leave your device. | |
| Open-source, auditable, and completely private. | |
| </p> | |
| <div class="flex flex-wrap justify-center gap-4 text-sm text-gray-500 dark:text-gray-400"> | |
| <div class="flex items-center space-x-2"> | |
| <i data-lucide="check-circle" class="w-4 h-4 text-green-500"></i> | |
| <span>AES-256-GCM</span> | |
| </div> | |
| <div class="flex items-center space-x-2"> | |
| <i data-lucide="check-circle" class="w-4 h-4 text-green-500"></i> | |
| <span>PBKDF2 600k Iterations</span> | |
| </div> | |
| <div class="flex items-center space-x-2"> | |
| <i data-lucide="check-circle" class="w-4 h-4 text-green-500"></i> | |
| <span>Open Source</span> | |
| </div> | |
| </div> | |
| </div> | |
| </section> | |
| <!-- Main Application --> | |
| <section class="pb-20 px-4 sm:px-6 lg:px-8"> | |
| <div class="max-w-6xl mx-auto"> | |
| <!-- Mode Tabs --> | |
| <div class="flex justify-center mb-8"> | |
| <div class="bg-white dark:bg-gray-800 p-1 rounded-2xl shadow-lg border border-gray-200 dark:border-gray-700 inline-flex"> | |
| <button onclick="switchMode('text')" id="tab-text" class="tab-active px-6 py-3 rounded-xl font-medium transition-all duration-200 flex items-center space-x-2"> | |
| <i data-lucide="type" class="w-4 h-4"></i> | |
| <span>Text</span> | |
| </button> | |
| <button onclick="switchMode('file')" id="tab-file" class="px-6 py-3 rounded-xl font-medium transition-all duration-200 flex items-center space-x-2 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200"> | |
| <i data-lucide="file-up" class="w-4 h-4"></i> | |
| <span>File</span> | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Operation Toggle --> | |
| <div class="flex justify-center mb-8"> | |
| <div class="bg-gray-100 dark:bg-gray-800 p-1 rounded-full inline-flex relative"> | |
| <div id="operation-indicator" class="absolute left-1 top-1 w-[calc(50%-4px)] h-[calc(100%-8px)] bg-white dark:bg-gray-700 rounded-full shadow-md transition-all duration-300"></div> | |
| <button onclick="setOperation('encrypt')" class="relative z-10 px-8 py-2 rounded-full font-medium transition-colors duration-200 flex items-center space-x-2" id="btn-encrypt"> | |
| <i data-lucide="lock" class="w-4 h-4"></i> | |
| <span>Encrypt</span> | |
| </button> | |
| <button onclick="setOperation('decrypt')" class="relative z-10 px-8 py-2 rounded-full font-medium transition-colors duration-200 flex items-center space-x-2 text-gray-600 dark:text-gray-400" id="btn-decrypt"> | |
| <i data-lucide="unlock" class="w-4 h-4"></i> | |
| <span>Decrypt</span> | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Text Mode Interface --> | |
| <div id="interface-text" class="glass-panel rounded-3xl p-8 shadow-2xl border border-gray-200 dark:border-gray-700"> | |
| <div class="grid md:grid-cols-2 gap-6"> | |
| <!-- Input Section --> | |
| <div class="space-y-4 h-full flex flex-col"> | |
| <div class="flex justify-between items-center h-8"> | |
| <label class="text-sm font-semibold text-gray-700 dark:text-gray-300 flex items-center space-x-2"> | |
| <i data-lucide="edit-3" class="w-4 h-4"></i> | |
| <span>Input</span> | |
| </label> | |
| <button onclick="clearText()" class="text-xs text-gray-500 hover:text-red-500 transition-colors px-2 py-1 rounded hover:bg-red-50 dark:hover:bg-red-900/20"> | |
| Clear | |
| </button> | |
| </div> | |
| <div class="relative flex-1"> | |
| <textarea | |
| id="text-input" | |
| placeholder="Enter text to encrypt or encrypted data to decrypt..." | |
| class="w-full h-64 p-4 rounded-xl bg-gray-50 dark:bg-gray-900 border-2 border-gray-200 dark:border-gray-700 focus:border-blue-500 focus:ring-0 resize-none transition-all font-mono text-sm" | |
| ></textarea> | |
| <div class="absolute bottom-4 right-4 text-xs text-gray-400 bg-gray-50 dark:bg-gray-900 px-2 py-1 rounded" id="char-count">0 chars</div> | |
| </div> | |
| </div> | |
| <!-- Output Section --> | |
| <div class="space-y-4 h-full flex flex-col"> | |
| <div class="flex justify-between items-center h-8"> | |
| <label class="text-sm font-semibold text-gray-700 dark:text-gray-300 flex items-center space-x-2"> | |
| <i data-lucide="shield" class="w-4 h-4"></i> | |
| <span>Result</span> | |
| </label> | |
| <div class="flex space-x-2"> | |
| <button onclick="copyOutput()" class="p-2 rounded-lg bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors" title="Copy to clipboard"> | |
| <i data-lucide="copy" class="w-4 h-4"></i> | |
| </button> | |
| <button onclick="downloadOutput()" class="p-2 rounded-lg bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors" title="Download as file"> | |
| <i data-lucide="download" class="w-4 h-4"></i> | |
| </button> | |
| </div> | |
| </div> | |
| <div class="relative flex-1"> | |
| <textarea | |
| id="text-output" | |
| readonly | |
| placeholder="Result will appear here..." | |
| class="w-full h-64 p-4 rounded-xl bg-gray-100 dark:bg-gray-800/50 border-2 border-gray-200 dark:border-gray-700 resize-none font-mono text-sm text-gray-600 dark:text-gray-400" | |
| ></textarea> | |
| <div id="output-overlay" class="absolute inset-0 flex items-center justify-center bg-gray-900/5 dark:bg-gray-900/20 rounded-xl backdrop-blur-sm hidden"> | |
| <div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Password Section --> | |
| <div class="mt-6 pt-6 border-t border-gray-200 dark:border-gray-700"> | |
| <div class="flex flex-col md:flex-row gap-4 items-start"> | |
| <div class="flex-1 w-full"> | |
| <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"> | |
| Password / Passphrase | |
| </label> | |
| <div class="relative"> | |
| <input | |
| type="password" | |
| id="password-input" | |
| placeholder="Enter strong password..." | |
| class="w-full px-4 py-3 rounded-xl bg-gray-50 dark:bg-gray-900 border-2 border-gray-200 dark:border-gray-700 focus:border-blue-500 focus:ring-0 transition-all pr-28" | |
| oninput="checkPasswordStrength()" | |
| > | |
| <button onclick="togglePasswordVisibility()" class="absolute right-14 top-1/2 -translate-y-1/2 p-2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-800 transition-colors" title="Show/Hide password"> | |
| <i data-lucide="eye" class="w-4 h-4" id="eye-icon"></i> | |
| </button> | |
| <button onclick="copyPassword()" class="absolute right-8 top-1/2 -translate-y-1/2 p-2 text-gray-400 hover:text-blue-600 dark:hover:text-blue-400 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-800 transition-colors" title="Copy password to clipboard"> | |
| <i data-lucide="copy" class="w-4 h-4"></i> | |
| </button> | |
| <button onclick="generatePassword()" class="absolute right-2 top-1/2 -translate-y-1/2 p-2 text-blue-500 hover:text-blue-700 rounded-lg hover:bg-blue-50 dark:hover:bg-blue-900/30 transition-colors" title="Generate secure password"> | |
| <i data-lucide="refresh-cw" class="w-4 h-4"></i> | |
| </button> | |
| </div> | |
| <!-- Password Strength --> | |
| <div class="mt-2 flex items-center space-x-3"> | |
| <div class="flex-1 h-2 bg-gray-200 dark:bg-gray-700 rounded-full overflow-hidden"> | |
| <div id="strength-bar" class="h-full w-0 transition-all duration-300 strength-weak"></div> | |
| </div> | |
| <span id="strength-text" class="text-xs font-medium text-gray-500 min-w-[50px] text-right">Empty</span> | |
| </div> | |
| </div> | |
| <div class="flex flex-col justify-end md:pt-7 w-full md:w-auto"> | |
| <button onclick="processText()" id="action-btn" class="w-full px-8 py-3 bg-gradient-to-r from-blue-600 to-purple-600 hover:from-blue-700 hover:to-purple-700 text-white font-semibold rounded-xl shadow-lg transform hover:scale-105 transition-all flex items-center justify-center space-x-2 min-w-[160px]"> | |
| <i data-lucide="lock" class="w-5 h-5" id="action-icon"></i> | |
| <span id="action-text">Encrypt</span> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- File Mode Interface --> | |
| <div id="interface-file" class="hidden glass-panel rounded-3xl p-8 shadow-2xl border border-gray-200 dark:border-gray-700"> | |
| <!-- File Drop Zone --> | |
| <div | |
| id="drop-zone" | |
| class="drop-zone border-3 border-dashed border-gray-300 dark:border-gray-600 rounded-2xl p-12 text-center cursor-pointer hover:border-blue-500 transition-all" | |
| onclick="document.getElementById('file-input').click()" | |
| ondrop="handleDrop(event)" | |
| ondragover="handleDragOver(event)" | |
| ondragleave="handleDragLeave(event)" | |
| > | |
| <input type="file" id="file-input" class="hidden" onchange="handleFileSelect(event)"> | |
| <div class="w-20 h-20 mx-auto mb-4 rounded-full bg-blue-100 dark:bg-blue-900/30 flex items-center justify-center"> | |
| <i data-lucide="upload-cloud" class="w-10 h-10 text-blue-600 dark:text-blue-400"></i> | |
| </div> | |
| <h3 class="text-lg font-semibold mb-2">Drop your file here</h3> | |
| <p class="text-gray-500 dark:text-gray-400 mb-4">or click to browse (Max 10MB)</p> | |
| <div id="file-info" class="hidden mt-4 p-4 bg-gray-50 dark:bg-gray-800 rounded-xl inline-flex items-center space-x-3"> | |
| <i data-lucide="file" class="w-8 h-8 text-blue-500"></i> | |
| <div class="text-left"> | |
| <div id="file-name" class="font-medium">filename.txt</div> | |
| <div id="file-size" class="text-sm text-gray-500">0 KB</div> | |
| </div> | |
| <button onclick="clearFile(event)" class="p-1 hover:bg-gray-200 dark:hover:bg-gray-700 rounded"> | |
| <i data-lucide="x" class="w-4 h-4"></i> | |
| </button> | |
| </div> | |
| </div> | |
| <!-- File Password & Action --> | |
| <div class="mt-6 flex flex-col md:flex-row gap-6 items-start"> | |
| <div class="flex-1 w-full"> | |
| <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"> | |
| Password Protection | |
| </label> | |
| <div class="relative"> | |
| <input | |
| type="password" | |
| id="file-password" | |
| placeholder="Enter password..." | |
| class="w-full px-4 py-3 rounded-xl bg-gray-50 dark:bg-gray-900 border-2 border-gray-200 dark:border-gray-700 focus:border-blue-500 focus:ring-0 transition-all pr-28" | |
| > | |
| <div class="absolute right-2 top-1/2 -translate-y-1/2 flex items-center space-x-1"> | |
| <button onclick="toggleFilePasswordVisibility()" class="p-2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-800 transition-colors" title="Show/Hide password"> | |
| <i data-lucide="eye" class="w-4 h-4" id="file-eye-icon"></i> | |
| </button> | |
| <button onclick="copyFilePassword()" class="p-2 text-gray-400 hover:text-blue-600 dark:hover:text-blue-400 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-800 transition-colors" title="Copy password to clipboard"> | |
| <i data-lucide="copy" class="w-4 h-4"></i> | |
| </button> | |
| <button onclick="generateFilePassword()" class="p-2 text-blue-500 hover:text-blue-700 rounded-lg hover:bg-blue-50 dark:hover:bg-blue-900/30 transition-colors" title="Generate secure password"> | |
| <i data-lucide="refresh-cw" class="w-4 h-4"></i> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="flex flex-col justify-end md:pt-7 w-full md:w-auto"> | |
| <button onclick="processFile()" id="file-action-btn" class="w-full md:w-auto px-8 py-3 bg-gradient-to-r from-blue-600 to-purple-600 hover:from-blue-700 hover:to-purple-700 text-white font-semibold rounded-xl shadow-lg transform hover:scale-105 transition-all flex items-center justify-center space-x-2 disabled:opacity-50 disabled:cursor-not-allowed min-w-[160px]" disabled> | |
| <i data-lucide="lock" class="w-5 h-5"></i> | |
| <span>Encrypt File</span> | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Progress Bar --> | |
| <div id="progress-container" class="hidden mt-6"> | |
| <div class="flex justify-between text-sm mb-2"> | |
| <span class="text-gray-600 dark:text-gray-400">Processing...</span> | |
| <span id="progress-text" class="font-medium">0%</span> | |
| </div> | |
| <div class="h-2 bg-gray-200 dark:bg-gray-700 rounded-full overflow-hidden"> | |
| <div id="progress-bar" class="h-full bg-gradient-to-r from-blue-500 to-purple-600 transition-all duration-300" style="width: 0%"></div> | |
| </div> | |
| </div> | |
| <!-- Download Section --> | |
| <div id="download-section" class="hidden mt-6 p-6 bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-xl"> | |
| <div class="flex items-center justify-between"> | |
| <div class="flex items-center space-x-3"> | |
| <div class="w-12 h-12 rounded-full bg-green-100 dark:bg-green-800 flex items-center justify-center"> | |
| <i data-lucide="check" class="w-6 h-6 text-green-600 dark:text-green-400"></i> | |
| </div> | |
| <div> | |
| <h4 class="font-semibold text-green-900 dark:text-green-100">Ready!</h4> | |
| <p class="text-sm text-green-700 dark:text-green-300">Your file has been processed successfully</p> | |
| </div> | |
| </div> | |
| <button onclick="downloadProcessedFile()" class="px-6 py-2 bg-green-600 hover:bg-green-700 text-white rounded-lg font-medium transition-colors flex items-center space-x-2"> | |
| <i data-lucide="download" class="w-4 h-4"></i> | |
| <span>Download</span> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </section> | |
| <!-- Security Specifications --> | |
| <section id="security" class="py-20 bg-white dark:bg-gray-800 border-y border-gray-200 dark:border-gray-700"> | |
| <div class="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8"> | |
| <div class="text-center mb-12"> | |
| <h2 class="text-3xl font-bold mb-4">Security Specifications</h2> | |
| <p class="text-gray-600 dark:text-gray-400">Enterprise-grade encryption standards</p> | |
| </div> | |
| <div class="grid md:grid-cols-2 lg:grid-cols-3 gap-6"> | |
| <!-- Spec Cards --> | |
| <div class="p-6 rounded-2xl bg-gray-50 dark:bg-gray-900 border border-gray-200 dark:border-gray-700 hover:shadow-lg transition-shadow"> | |
| <div class="w-12 h-12 rounded-xl bg-blue-100 dark:bg-blue-900/30 flex items-center justify-center mb-4"> | |
| <i data-lucide="cpu" class="w-6 h-6 text-blue-600"></i> | |
| </div> | |
| <h3 class="font-semibold mb-2">Algorithm</h3> | |
| <p class="text-2xl font-bold text-blue-600">AES-256-GCM</p> | |
| <p class="text-sm text-gray-500 mt-1">Galois/Counter Mode</p> | |
| </div> | |
| <div class="p-6 rounded-2xl bg-gray-50 dark:bg-gray-900 border border-gray-200 dark:border-gray-700 hover:shadow-lg transition-shadow"> | |
| <div class="w-12 h-12 rounded-xl bg-purple-100 dark:bg-purple-900/30 flex items-center justify-center mb-4"> | |
| <i data-lucide="key" class="w-6 h-6 text-purple-600"></i> | |
| </div> | |
| <h3 class="font-semibold mb-2">Key Derivation</h3> | |
| <p class="text-2xl font-bold text-purple-600">PBKDF2</p> | |
| <p class="text-sm text-gray-500 mt-1">HMAC-SHA256</p> | |
| </div> | |
| <div class="p-6 rounded-2xl bg-gray-50 dark:bg-gray-900 border border-gray-200 dark:border-gray-700 hover:shadow-lg transition-shadow"> | |
| <div class="w-12 h-12 rounded-xl bg-green-100 dark:bg-green-900/30 flex items-center justify-center mb-4"> | |
| <i data-lucide="repeat" class="w-6 h-6 text-green-600"></i> | |
| </div> | |
| <h3 class="font-semibold mb-2">Iterations</h3> | |
| <p class="text-2xl font-bold text-green-600">600,000</p> | |
| <p class="text-sm text-gray-500 mt-1">Brute-force resistant</p> | |
| </div> | |
| <div class="p-6 rounded-2xl bg-gray-50 dark:bg-gray-900 border border-gray-200 dark:border-gray-700 hover:shadow-lg transition-shadow"> | |
| <div class="w-12 h-12 rounded-xl bg-orange-100 dark:bg-orange-900/30 flex items-center justify-center mb-4"> | |
| <i data-lucide="fingerprint" class="w-6 h-6 text-orange-600"></i> | |
| </div> | |
| <h3 class="font-semibold mb-2">Salt Length</h3> | |
| <p class="text-2xl font-bold text-orange-600">16 bytes</p> | |
| <p class="text-sm text-gray-500 mt-1">128-bit random</p> | |
| </div> | |
| <div class="p-6 rounded-2xl bg-gray-50 dark:bg-gray-900 border border-gray-200 dark:border-gray-700 hover:shadow-lg transition-shadow"> | |
| <div class="w-12 h-12 rounded-xl bg-red-100 dark:bg-red-900/30 flex items-center justify-center mb-4"> | |
| <i data-lucide="shuffle" class="w-6 h-6 text-red-600"></i> | |
| </div> | |
| <h3 class="font-semibold mb-2">IV Length</h3> | |
| <p class="text-2xl font-bold text-red-600">12 bytes</p> | |
| <p class="text-sm text-gray-500 mt-1">96-bit nonce</p> | |
| </div> | |
| <div class="p-6 rounded-2xl bg-gray-50 dark:bg-gray-900 border border-gray-200 dark:border-gray-700 hover:shadow-lg transition-shadow"> | |
| <div class="w-12 h-12 rounded-xl bg-indigo-100 dark:bg-indigo-900/30 flex items-center justify-center mb-4"> | |
| <i data-lucide="badge-check" class="w-6 h-6 text-indigo-600"></i> | |
| </div> | |
| <h3 class="font-semibold mb-2">Auth Tag</h3> | |
| <p class="text-2xl font-bold text-indigo-600">128-bit</p> | |
| <p class="text-sm text-gray-500 mt-1">GCM Authentication</p> | |
| </div> | |
| </div> | |
| <!-- Data Format --> | |
| <div class="mt-12 p-6 rounded-2xl bg-gray-900 text-gray-100 font-mono text-sm overflow-x-auto"> | |
| <div class="flex items-center justify-between mb-4"> | |
| <span class="text-gray-400">Data Container Format</span> | |
| <span class="text-xs bg-gray-800 px-2 py-1 rounded">ENCv1</span> | |
| </div> | |
| <code class="block text-green-400"> | |
| ENCv1:{Base64(JSON header + newline + ciphertext)} | |
| </code> | |
| <div class="mt-4 text-gray-400 text-xs"> | |
| { "v": 1, "alg": "AES-GCM", "kdf": {"name": "PBKDF2", "hash": "SHA-256", "iters": 600000, "salt_b64": "β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’β’"}, "iv_b64": "β’β’β’β’β’β’β’β’β’β’β’β’", "keyType": "passphrase", "created": "2025-01-15T12:00:00Z", "type": "text", "orig": null } | |
| </div> | |
| </div> | |
| </div> | |
| </section> | |
| <!-- Features Grid --> | |
| <section class="py-20 px-4 sm:px-6 lg:px-8"> | |
| <div class="max-w-6xl mx-auto"> | |
| <div class="grid md:grid-cols-3 gap-8"> | |
| <div class="text-center p-6"> | |
| <div class="w-16 h-16 mx-auto mb-4 rounded-2xl bg-gradient-to-br from-blue-500 to-blue-600 flex items-center justify-center shadow-lg"> | |
| <i data-lucide="server-off" class="w-8 h-8 text-white"></i> | |
| </div> | |
| <h3 class="text-xl font-bold mb-2">Zero Server Storage</h3> | |
| <p class="text-gray-600 dark:text-gray-400">All encryption happens locally in your browser. No data is ever transmitted to any server.</p> | |
| </div> | |
| <div class="text-center p-6"> | |
| <div class="w-16 h-16 mx-auto mb-4 rounded-2xl bg-gradient-to-br from-purple-500 to-purple-600 flex items-center justify-center shadow-lg"> | |
| <i data-lucide="code" class="w-8 h-8 text-white"></i> | |
| </div> | |
| <h3 class="text-xl font-bold mb-2">Open Source</h3> | |
| <p class="text-gray-600 dark:text-gray-400">Fully auditable code. Verify the encryption implementation yourself. No backdoors possible.</p> | |
| </div> | |
| <div class="text-center p-6"> | |
| <div class="w-16 h-16 mx-auto mb-4 rounded-2xl bg-gradient-to-br from-green-500 to-green-600 flex items-center justify-center shadow-lg"> | |
| <i data-lucide="zap" class="w-8 h-8 text-white"></i> | |
| </div> | |
| <h3 class="text-xl font-bold mb-2">Fast & Efficient</h3> | |
| <p class="text-gray-600 dark:text-gray-400">Optimized WebCrypto API implementation. Handle files up to 10MB with ease.</p> | |
| </div> | |
| </div> | |
| </div> | |
| </section> | |
| <!-- Footer --> | |
| <footer class="bg-gray-900 text-gray-400 py-12 border-t border-gray-800"> | |
| <div class="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8"> | |
| <div class="flex flex-col md:flex-row justify-between items-center"> | |
| <div class="flex items-center space-x-3 mb-4 md:mb-0"> | |
| <div class="w-8 h-8 bg-gradient-to-br from-blue-500 to-purple-600 rounded-lg flex items-center justify-center"> | |
| <i data-lucide="shield-check" class="w-5 h-5 text-white"></i> | |
| </div> | |
| <span class="text-lg font-bold text-white">SecureCrypt</span> | |
| </div> | |
| <div class="text-sm"> | |
| <p>Client-side encryption tool. No data leaves your device.</p> | |
| </div> | |
| </div> | |
| <div class="mt-8 pt-8 border-t border-gray-800 text-xs text-center"> | |
| <p class="mb-2"><strong>Security Notice:</strong> All operations occur locally. No key recovery possible. Protects at-rest data only.</p> | |
| <p>© 2025 SecureCrypt. Open source encryption utility.</p> | |
| </div> | |
| </div> | |
| </footer> | |
| <!-- Toast Container --> | |
| <div id="toast-container" class="fixed bottom-4 right-4 z-50 flex flex-col space-y-2"></div> | |
| <script> | |
| // Initialize Lucide icons | |
| lucide.createIcons(); | |
| // State management | |
| let currentMode = 'text'; | |
| let currentOperation = 'encrypt'; | |
| let selectedFile = null; | |
| let processedFileData = null; | |
| // Theme toggle | |
| function toggleTheme() { | |
| if (document.documentElement.classList.contains('dark')) { | |
| document.documentElement.classList.remove('dark'); | |
| localStorage.theme = 'light'; | |
| } else { | |
| document.documentElement.classList.add('dark'); | |
| localStorage.theme = 'dark'; | |
| } | |
| } | |
| // Initialize theme | |
| if (localStorage.theme === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) { | |
| document.documentElement.classList.add('dark'); | |
| } | |
| // Mode switching | |
| function switchMode(mode) { | |
| currentMode = mode; | |
| // Update tabs | |
| document.getElementById('tab-text').className = mode === 'text' | |
| ? 'tab-active px-6 py-3 rounded-xl font-medium transition-all duration-200 flex items-center space-x-2' | |
| : 'px-6 py-3 rounded-xl font-medium transition-all duration-200 flex items-center space-x-2 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200'; | |
| document.getElementById('tab-file').className = mode === 'file' | |
| ? 'tab-active px-6 py-3 rounded-xl font-medium transition-all duration-200 flex items-center space-x-2' | |
| : 'px-6 py-3 rounded-xl font-medium transition-all duration-200 flex items-center space-x-2 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200'; | |
| // Show/hide interfaces | |
| document.getElementById('interface-text').classList.toggle('hidden', mode !== 'text'); | |
| document.getElementById('interface-file').classList.toggle('hidden', mode !== 'file'); | |
| } | |
| // Operation switching | |
| function setOperation(op) { | |
| currentOperation = op; | |
| const indicator = document.getElementById('operation-indicator'); | |
| const btnEncrypt = document.getElementById('btn-encrypt'); | |
| const btnDecrypt = document.getElementById('btn-decrypt'); | |
| const actionBtn = document.getElementById('action-btn'); | |
| const actionIcon = document.getElementById('action-icon'); | |
| const actionText = document.getElementById('action-text'); | |
| const fileBtn = document.getElementById('file-action-btn'); | |
| if (op === 'encrypt') { | |
| indicator.style.left = '4px'; | |
| btnEncrypt.classList.remove('text-gray-600', 'dark:text-gray-400'); | |
| btnDecrypt.classList.add('text-gray-600', 'dark:text-gray-400'); | |
| actionBtn.classList.remove('from-amber-500', 'to-orange-600'); | |
| actionBtn.classList.add('from-blue-600', 'to-purple-600'); | |
| actionIcon.setAttribute('data-lucide', 'lock'); | |
| actionText.textContent = 'Encrypt'; | |
| if (fileBtn) { | |
| fileBtn.innerHTML = '<i data-lucide="lock" class="w-5 h-5"></i><span>Encrypt File</span>'; | |
| fileBtn.classList.remove('from-amber-500', 'to-orange-600'); | |
| fileBtn.classList.add('from-blue-600', 'to-purple-600'); | |
| } | |
| } else { | |
| indicator.style.left = 'calc(50% + 4px)'; | |
| btnDecrypt.classList.remove('text-gray-600', 'dark:text-gray-400'); | |
| btnEncrypt.classList.add('text-gray-600', 'dark:text-gray-400'); | |
| actionBtn.classList.remove('from-blue-600', 'to-purple-600'); | |
| actionBtn.classList.add('from-amber-500', 'to-orange-600'); | |
| actionIcon.setAttribute('data-lucide', 'unlock'); | |
| actionText.textContent = 'Decrypt'; | |
| if (fileBtn) { | |
| fileBtn.innerHTML = '<i data-lucide="unlock" class="w-5 h-5"></i><span>Decrypt File</span>'; | |
| fileBtn.classList.remove('from-blue-600', 'to-purple-600'); | |
| fileBtn.classList.add('from-amber-500', 'to-orange-600'); | |
| } | |
| } | |
| lucide.createIcons(); | |
| } | |
| // Password strength checker | |
| function checkPasswordStrength() { | |
| const password = document.getElementById('password-input').value; | |
| const bar = document.getElementById('strength-bar'); | |
| const text = document.getElementById('strength-text'); | |
| let strength = 0; | |
| if (password.length > 8) strength++; | |
| if (password.length > 12) strength++; | |
| if (/[A-Z]/.test(password)) strength++; | |
| if (/[0-9]/.test(password)) strength++; | |
| if (/[^A-Za-z0-9]/.test(password)) strength++; | |
| bar.className = 'h-full transition-all duration-300 '; | |
| if (password.length === 0) { | |
| bar.style.width = '0%'; | |
| text.textContent = 'Empty'; | |
| text.className = 'text-xs font-medium text-gray-500'; | |
| } else if (strength < 2) { | |
| bar.style.width = '25%'; | |
| bar.classList.add('strength-weak'); | |
| text.textContent = 'Weak'; | |
| text.className = 'text-xs font-medium text-red-500'; | |
| } else if (strength < 4) { | |
| bar.style.width = '50%'; | |
| bar.classList.add('strength-fair'); | |
| text.textContent = 'Fair'; | |
| text.className = 'text-xs font-medium text-amber-500'; | |
| } else if (strength < 5) { | |
| bar.style.width = '75%'; | |
| bar.classList.add('strength-good'); | |
| text.textContent = 'Good'; | |
| text.className = 'text-xs font-medium text-blue-500'; | |
| } else { | |
| bar.style.width = '100%'; | |
| bar.classList.add('strength-strong'); | |
| text.textContent = 'Strong'; | |
| text.className = 'text-xs font-medium text-green-500'; | |
| } | |
| } | |
| // Generate secure password | |
| function generatePassword() { | |
| const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()_+~`|}{[]:;?><,./-='; | |
| let password = ''; | |
| const length = 24; | |
| const array = new Uint32Array(length); | |
| crypto.getRandomValues(array); | |
| for (let i = 0; i < length; i++) { | |
| password += chars[array[i] % chars.length]; | |
| } | |
| document.getElementById('password-input').value = password; | |
| checkPasswordStrength(); | |
| showToast('Password generated!', 'success'); | |
| } | |
| function generateFilePassword() { | |
| generatePassword(); | |
| document.getElementById('file-password').value = document.getElementById('password-input').value; | |
| } | |
| // Toggle password visibility | |
| function togglePasswordVisibility() { | |
| const input = document.getElementById('password-input'); | |
| const icon = document.getElementById('eye-icon'); | |
| if (input.type === 'password') { | |
| input.type = 'text'; | |
| icon.setAttribute('data-lucide', 'eye-off'); | |
| } else { | |
| input.type = 'password'; | |
| icon.setAttribute('data-lucide', 'eye'); | |
| } | |
| lucide.createIcons(); | |
| } | |
| // Toggle file password visibility | |
| function toggleFilePasswordVisibility() { | |
| const input = document.getElementById('file-password'); | |
| const icon = document.getElementById('file-eye-icon'); | |
| if (input.type === 'password') { | |
| input.type = 'text'; | |
| icon.setAttribute('data-lucide', 'eye-off'); | |
| } else { | |
| input.type = 'password'; | |
| icon.setAttribute('data-lucide', 'eye'); | |
| } | |
| lucide.createIcons(); | |
| } | |
| // Copy password to clipboard | |
| function copyPassword() { | |
| const input = document.getElementById('password-input'); | |
| if (!input.value) { | |
| showToast('No password to copy', 'error'); | |
| return; | |
| } | |
| input.select(); | |
| document.execCommand('copy'); | |
| showToast('Password copied to clipboard!', 'success'); | |
| } | |
| // Copy file password to clipboard | |
| function copyFilePassword() { | |
| const input = document.getElementById('file-password'); | |
| if (!input.value) { | |
| showToast('No password to copy', 'error'); | |
| return; | |
| } | |
| input.select(); | |
| document.execCommand('copy'); | |
| showToast('Password copied to clipboard!', 'success'); | |
| } | |
| // Character count | |
| document.getElementById('text-input').addEventListener('input', function() { | |
| document.getElementById('char-count').textContent = this.value.length + ' chars'; | |
| }); | |
| // Clear functions | |
| function clearText() { | |
| document.getElementById('text-input').value = ''; | |
| document.getElementById('text-output').value = ''; | |
| document.getElementById('char-count').textContent = '0 chars'; | |
| } | |
| // Crypto functions | |
| async function deriveKey(password, salt) { | |
| const encoder = new TextEncoder(); | |
| const keyMaterial = await crypto.subtle.importKey( | |
| 'raw', | |
| encoder.encode(password), | |
| { name: 'PBKDF2' }, | |
| false, | |
| ['deriveKey'] | |
| ); | |
| return crypto.subtle.deriveKey( | |
| { | |
| name: 'PBKDF2', | |
| salt: salt, | |
| iterations: 600000, | |
| hash: 'SHA-256' | |
| }, | |
| keyMaterial, | |
| { name: 'AES-GCM', length: 256 }, | |
| false, | |
| ['encrypt', 'decrypt'] | |
| ); | |
| } | |
| async function encryptText(text, password) { | |
| const encoder = new TextEncoder(); | |
| const salt = crypto.getRandomValues(new Uint8Array(16)); | |
| const iv = crypto.getRandomValues(new Uint8Array(12)); | |
| const key = await deriveKey(password, salt); | |
| const encrypted = await crypto.subtle.encrypt( | |
| { name: 'AES-GCM', iv: iv }, | |
| key, | |
| encoder.encode(text) | |
| ); | |
| const header = { | |
| v: 1, | |
| alg: 'AES-GCM', | |
| kdf: { | |
| name: 'PBKDF2', | |
| hash: 'SHA-256', | |
| iters: 600000, | |
| salt_b64: btoa(String.fromCharCode(...salt)) | |
| }, | |
| iv_b64: btoa(String.fromCharCode(...iv)), | |
| keyType: 'passphrase', | |
| created: new Date().toISOString(), | |
| type: 'text', | |
| orig: null | |
| }; | |
| const ciphertext = btoa(String.fromCharCode(...new Uint8Array(encrypted))); | |
| return 'ENCv1:' + btoa(JSON.stringify(header) + '\n' + ciphertext); | |
| } | |
| async function decryptText(encryptedData, password) { | |
| if (!encryptedData.startsWith('ENCv1:')) { | |
| throw new Error('Invalid format'); | |
| } | |
| const payload = atob(encryptedData.slice(6)); | |
| const newlineIndex = payload.indexOf('\n'); | |
| const header = JSON.parse(payload.slice(0, newlineIndex)); | |
| const ciphertext = payload.slice(newlineIndex + 1); | |
| const salt = Uint8Array.from(atob(header.kdf.salt_b64), c => c.charCodeAt(0)); | |
| const iv = Uint8Array.from(atob(header.iv_b64), c => c.charCodeAt(0)); | |
| const key = await deriveKey(password, salt); | |
| const decrypted = await crypto.subtle.decrypt( | |
| { name: 'AES-GCM', iv: iv }, | |
| key, | |
| Uint8Array.from(atob(ciphertext), c => c.charCodeAt(0)) | |
| ); | |
| return new TextDecoder().decode(decrypted); | |
| } | |
| // Process text | |
| async function processText() { | |
| const input = document.getElementById('text-input').value; | |
| const password = document.getElementById('password-input').value; | |
| const output = document.getElementById('text-output'); | |
| const overlay = document.getElementById('output-overlay'); | |
| if (!input || !password) { | |
| showToast('Please enter both text and password', 'error'); | |
| return; | |
| } | |
| overlay.classList.remove('hidden'); | |
| try { | |
| if (currentOperation === 'encrypt') { | |
| const result = await encryptText(input, password); | |
| output.value = result; | |
| showToast('Text encrypted successfully!', 'success'); | |
| } else { | |
| const result = await decryptText(input, password); | |
| output.value = result; | |
| showToast('Text decrypted successfully!', 'success'); | |
| } | |
| } catch (error) { | |
| showToast('Error: ' + (currentOperation === 'decrypt' ? 'Invalid password or corrupted data' : error.message), 'error'); | |
| } finally { | |
| overlay.classList.add('hidden'); | |
| } | |
| } | |
| // Copy output | |
| function copyOutput() { | |
| const output = document.getElementById('text-output'); | |
| if (!output.value) return; | |
| output.select(); | |
| document.execCommand('copy'); | |
| showToast('Copied to clipboard!', 'success'); | |
| } | |
| // Download output | |
| function downloadOutput() { | |
| const output = document.getElementById('text-output').value; | |
| if (!output) return; | |
| const blob = new Blob([output], { type: 'text/plain' }); | |
| const url = URL.createObjectURL(blob); | |
| const a = document.createElement('a'); | |
| a.href = url; | |
| a.download = currentOperation === 'encrypt' ? 'encrypted.txt' : 'decrypted.txt'; | |
| a.click(); | |
| URL.revokeObjectURL(url); | |
| showToast('File downloaded!', 'success'); | |
| } | |
| // File handling | |
| function handleDragOver(e) { | |
| e.preventDefault(); | |
| e.currentTarget.classList.add('drag-over'); | |
| } | |
| function handleDragLeave(e) { | |
| e.currentTarget.classList.remove('drag-over'); | |
| } | |
| function handleDrop(e) { | |
| e.preventDefault(); | |
| e.currentTarget.classList.remove('drag-over'); | |
| const files = e.dataTransfer.files; | |
| if (files.length > 0) { | |
| processSelectedFile(files[0]); | |
| } | |
| } | |
| function handleFileSelect(e) { | |
| if (e.target.files.length > 0) { | |
| processSelectedFile(e.target.files[0]); | |
| } | |
| } | |
| function processSelectedFile(file) { | |
| if (file.size > 10 * 1024 * 1024) { | |
| showToast('File too large. Max 10MB allowed.', 'error'); | |
| return; | |
| } | |
| selectedFile = file; | |
| document.getElementById('file-info').classList.remove('hidden'); | |
| document.getElementById('file-name').textContent = file.name; | |
| document.getElementById('file-size').textContent = (file.size / 1024).toFixed(1) + ' KB'; | |
| document.getElementById('file-action-btn').disabled = false; | |
| } | |
| function clearFile(e) { | |
| e.stopPropagation(); | |
| selectedFile = null; | |
| document.getElementById('file-info').classList.add('hidden'); | |
| document.getElementById('file-input').value = ''; | |
| document.getElementById('file-action-btn').disabled = true; | |
| document.getElementById('download-section').classList.add('hidden'); | |
| } | |
| async function processFile() { | |
| if (!selectedFile) return; | |
| const password = document.getElementById('file-password').value; | |
| if (!password) { | |
| showToast('Please enter a password', 'error'); | |
| return; | |
| } | |
| const progressContainer = document.getElementById('progress-container'); | |
| const progressBar = document.getElementById('progress-bar'); | |
| const progressText = document.getElementById('progress-text'); | |
| const btn = document.getElementById('file-action-btn'); | |
| progressContainer.classList.remove('hidden'); | |
| btn.disabled = true; | |
| try { | |
| const reader = new FileReader(); | |
| reader.onprogress = (e) => { | |
| if (e.lengthComputable) { | |
| const percent = (e.loaded / e.total) * 50; | |
| progressBar.style.width = percent + '%'; | |
| progressText.textContent = Math.round(percent) + '%'; | |
| } | |
| }; | |
| reader.onload = async (e) => { | |
| const content = e.target.result; | |
| try { | |
| let result; | |
| if (currentOperation === 'encrypt') { | |
| const bytes = new Uint8Array(content); | |
| const base64 = btoa(String.fromCharCode(...bytes)); | |
| result = await encryptText(base64, password); | |
| } else { | |
| const decrypted = await decryptText(content, password); | |
| result = Uint8Array.from(atob(decrypted), c => c.charCodeAt(0)); | |
| } | |
| progressBar.style.width = '100%'; | |
| progressText.textContent = '100%'; | |
| processedFileData = result; | |
| document.getElementById('download-section').classList.remove('hidden'); | |
| showToast('File processed successfully!', 'success'); | |
| } catch (error) { | |
| showToast('Error processing file: ' + error.message, 'error'); | |
| } | |
| }; | |
| if (currentOperation === 'encrypt') { | |
| reader.readAsArrayBuffer(selectedFile); | |
| } else { | |
| reader.readAsText(selectedFile); | |
| } | |
| } catch (error) { | |
| showToast('Error: ' + error.message, 'error'); | |
| btn.disabled = false; | |
| } | |
| } | |
| function downloadProcessedFile() { | |
| if (!processedFileData) return; | |
| let blob, filename; | |
| if (currentOperation === 'encrypt') { | |
| blob = new Blob([processedFileData], { type: 'text/plain' }); | |
| filename = selectedFile.name + '.encrypted'; | |
| } else { | |
| blob = new Blob([processedFileData], { type: 'application/octet-stream' }); | |
| filename = selectedFile.name.replace('.encrypted', '') + '.decrypted'; | |
| } | |
| const url = URL.createObjectURL(blob); | |
| const a = document.createElement('a'); | |
| a.href = url; | |
| a.download = filename; | |
| a.click(); | |
| URL.revokeObjectURL(url); | |
| showToast('Download started!', 'success'); | |
| } | |
| // Toast notifications | |
| function showToast(message, type = 'info') { | |
| const container = document.getElementById('toast-container'); | |
| const toast = document.createElement('div'); | |
| const colors = { | |
| success: 'bg-green-500', | |
| error: 'bg-red-500', | |
| info: 'bg-blue-500' | |
| }; | |
| const icons = { | |
| success: 'check-circle', | |
| error: 'x-circle', | |
| info: 'info' | |
| }; | |
| toast.className = `toast ${colors[type]} text-white px-6 py-3 rounded-xl shadow-lg flex items-center space-x-2 min-w-[300px]`; | |
| toast.innerHTML = ` | |
| <i data-lucide="${icons[type]}" class="w-5 h-5"></i> | |
| <span class="font-medium">${message}</span> | |
| `; | |
| container.appendChild(toast); | |
| lucide.createIcons(); | |
| setTimeout(() => { | |
| toast.style.opacity = '0'; | |
| toast.style.transform = 'translateX(100%)'; | |
| setTimeout(() => toast.remove(), 300); | |
| }, 3000); | |
| } | |
| // Keyboard shortcuts | |
| document.addEventListener('keydown', (e) => { | |
| if (e.ctrlKey || e.metaKey) { | |
| if (e.key === 'Enter') { | |
| if (currentMode === 'text') { | |
| processText(); | |
| } else { | |
| processFile(); | |
| } | |
| } | |
| } | |
| }); | |
| </script> | |
| <script src="https://deepsite.hf.co/deepsite-badge.js"></script> | |
| </body> | |
| </html> |