From 52778d181bb445e87f01d038aeaee91c8d03f27f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=9Cmit=20Tun=C3=A7?= Date: Sat, 23 May 2026 10:48:45 +0300 Subject: [PATCH] feat: build premium neon glassmorphic UI and interactive cropper with pixel auto-trim --- index.html | 338 +++++++++++++++ renderer.js | 763 ++++++++++++++++++++++++++++++++++ styles.css | 1134 +++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 2235 insertions(+) create mode 100644 index.html create mode 100644 renderer.js create mode 100644 styles.css diff --git a/index.html b/index.html new file mode 100644 index 0000000..806c782 --- /dev/null +++ b/index.html @@ -0,0 +1,338 @@ + + + + + + Dekupai - Turunçgil AI Dekupaj + + + + + + + + + + + +
+
+ + +
+ +
+ + + +
+
+ + + + + +
+ +
+ +
+
+
+ + + + + +
+

Drag & Drop Image Here

+

Supports PNG, JPG, JPEG, or WebP formats

+
OR
+ +
+
+ + + +
+ + + +
+ + + + + + + + + diff --git a/renderer.js b/renderer.js new file mode 100644 index 0000000..169e283 --- /dev/null +++ b/renderer.js @@ -0,0 +1,763 @@ +// Aether BG Renderer Processes + +// --- GLOBAL VARIABLES & STATE --- +let cropper = null; +let currentImagePath = null; +let originalFileName = ''; +let processedTempPath = null; +let activeBgMode = 'transparent'; // 'transparent', 'solid', 'gradient' +let activeSolidColor = '#ffffff'; +let activeGradientStops = [ + { offset: 0, color: '#a78bfa' }, + { offset: 1, color: '#06b6d4' } +]; +let scaleX = 1; +let scaleY = 1; + +// Define gradient templates for canvas exporting +const GRADIENTS = { + 'grad-1': [{ offset: 0, color: '#a78bfa' }, { offset: 1, color: '#06b6d4' }], // Violet Cyan + 'grad-2': [{ offset: 0, color: '#f43f5e' }, { offset: 1, color: '#f97316' }], // Sunset + 'grad-3': [{ offset: 0, color: '#10b981' }, { offset: 1, color: '#06b6d4' }], // Emerald + 'grad-4': [{ offset: 0, color: '#ec4899' }, { offset: 1, color: '#8b5cf6' }], // Cyberpunk + 'grad-5': [{ offset: 0, color: '#3b82f6' }, { offset: 1, color: '#8b5cf6' }], // Aether Blue + 'grad-6': [{ offset: 0, color: '#f59e0b' }, { offset: 1, color: '#ef4444' }] // Warm Ember +}; + +// --- DOM ELEMENTS SELECTION --- +const elements = { + // Titlebar + winMin: document.getElementById('win-min'), + winMax: document.getElementById('win-max'), + winClose: document.getElementById('win-close'), + + // Setup Wizard + setupOverlay: document.getElementById('setup-overlay'), + btnStartSetup: document.getElementById('btn-start-setup'), + setupProgressBar: document.getElementById('setup-progress-bar'), + setupStatusText: document.getElementById('setup-status-text'), + stepVenv: document.getElementById('step-venv'), + stepPip: document.getElementById('step-pip'), + stepDeps: document.getElementById('step-deps'), + stepLibs: document.getElementById('step-libs'), + + // Workspace Views + dropzone: document.getElementById('dropzone'), + btnBrowse: document.getElementById('btn-browse'), + editorContainer: document.getElementById('editor-container'), + canvasWrapper: document.getElementById('canvas-wrapper'), + imageElement: document.getElementById('image-element'), + canvasBgOverlay: document.getElementById('canvas-bg-overlay'), + + // Workspace Toolbar Controls + ctrlCropDrag: document.getElementById('ctrl-crop-drag'), + ctrlCropBox: document.getElementById('ctrl-crop-box'), + ctrlRotL: document.getElementById('ctrl-rot-l'), + ctrlRotR: document.getElementById('ctrl-rot-r'), + ctrlFlipH: document.getElementById('ctrl-flip-h'), + ctrlFlipV: document.getElementById('ctrl-flip-v'), + cropRatioSelect: document.getElementById('crop-ratio-select'), + ctrlAutoTrim: document.getElementById('ctrl-auto-trim'), + ctrlReset: document.getElementById('ctrl-reset'), + + // Control Tower: Info Panel + infoCardEmpty: document.getElementById('info-card-empty'), + infoCardLoaded: document.getElementById('info-card-loaded'), + infoFilename: document.getElementById('info-filename'), + infoResolution: document.getElementById('info-resolution'), + infoSize: document.getElementById('info-size'), + + // Control Tower: AI Processor + btnRemoveBg: document.getElementById('btn-remove-bg'), + loaderBox: document.getElementById('loader-box'), + loaderText: document.getElementById('loader-text'), + loaderSubtext: document.getElementById('loader-subtext'), + + // Control Tower: Background settings + secCanvasBg: document.getElementById('sec-canvas-bg'), + tabBgTrans: document.getElementById('tab-bg-trans'), + tabBgSolid: document.getElementById('tab-bg-solid'), + tabBgGrad: document.getElementById('tab-bg-grad'), + subpanelSolid: document.getElementById('subpanel-solid'), + subpanelGradient: document.getElementById('subpanel-gradient'), + customColorPicker: document.getElementById('custom-color-picker'), + btnCustomColorPicker: document.getElementById('btn-custom-color-picker'), + + // Control Tower: Export Studio + secExport: document.getElementById('sec-export'), + exportFormat: document.getElementById('export-format'), + rowQualitySlider: document.getElementById('row-quality-slider'), + exportQuality: document.getElementById('export-quality'), + exportQualityVal: document.getElementById('export-quality-val'), + btnExport: document.getElementById('btn-export'), + + // Toast Banner + toastBanner: document.getElementById('toast-banner'), + toastTitle: document.getElementById('toast-title'), + toastMessage: document.getElementById('toast-message'), + btnCloseToast: document.getElementById('btn-close-toast') +}; + +// --- INITIALIZE & ENVIRONMENT CHECKS --- +document.addEventListener('DOMContentLoaded', async () => { + setupTitlebarListeners(); + setupToastListeners(); + + // Check if Python virtual environment exists + const { hasVenv } = await window.api.checkPythonSetup(); + if (!hasVenv) { + elements.setupOverlay.classList.remove('hidden'); + } else { + initWorkspace(); + } +}); + +// Titlebar Controls +function setupTitlebarListeners() { + elements.winMin.addEventListener('click', () => window.api.minimizeWindow()); + elements.winMax.addEventListener('click', () => window.api.maximizeWindow()); + elements.winClose.addEventListener('click', () => window.api.closeWindow()); +} + +// Workspace Initialization +function initWorkspace() { + setupDragAndDrop(); + setupToolbarListeners(); + setupControlTowerListeners(); + setupBgSwatches(); + + // Browse Button click + elements.btnBrowse.addEventListener('click', selectLocalImage); + elements.dropzone.addEventListener('click', (e) => { + // Only open if clicking container directly and not buttons + if (e.target === elements.dropzone || e.target.closest('.dropzone-content') && !e.target.closest('button')) { + selectLocalImage(); + } + }); +} + +// --- PYTHON VENV INSTALLER ENGINE --- +elements.btnStartSetup.addEventListener('click', async () => { + elements.btnStartSetup.disabled = true; + elements.setupProgressBar.parentElement.style.display = 'block'; + elements.setupProgressBar.style.width = '5%'; + + // Track active steps + const setStepActive = (stepEl) => { + document.querySelectorAll('.step-item').forEach(el => el.classList.remove('active')); + stepEl.classList.add('active'); + }; + + const setStepDone = (stepEl) => { + stepEl.classList.remove('active'); + stepEl.classList.add('done'); + }; + + const setStepFailed = (stepEl) => { + stepEl.classList.remove('active'); + stepEl.classList.add('failed'); + }; + + setStepActive(elements.stepVenv); + + // Listen for pipeline setup output logs + window.api.onSetupProgress((progressText) => { + elements.setupStatusText.innerText = progressText; + + // Parse step state + if (progressText.includes("Creating Python virtual environment")) { + setStepActive(elements.stepVenv); + elements.setupProgressBar.style.width = '15%'; + } else if (progressText.includes("Virtual environment successfully created") || progressText.includes("Virtual environment already exists")) { + setStepDone(elements.stepVenv); + setStepActive(elements.stepPip); + elements.setupProgressBar.style.width = '30%'; + } else if (progressText.includes("Upgrading pip")) { + setStepActive(elements.stepPip); + elements.setupProgressBar.style.width = '45%'; + } else if (progressText.includes("Installing PyTorch")) { + setStepDone(elements.stepPip); + setStepActive(elements.stepDeps); + elements.setupProgressBar.style.width = '60%'; + } else if (progressText.includes("Downloading torch") || progressText.includes("Installing collected packages")) { + elements.setupProgressBar.style.width = '75%'; + } else if (progressText.includes("Setup completed successfully")) { + setStepDone(elements.stepDeps); + setStepDone(elements.stepLibs); + elements.setupProgressBar.style.width = '100%'; + } + }); + + try { + const result = await window.api.runPythonSetup(); + if (result.code === 0) { + elements.setupStatusText.innerText = "Configuration Complete! Enjoy background removal."; + showToast("Setup Success", "Python environment is fully configured.", "success"); + + setTimeout(() => { + elements.setupOverlay.classList.add('hidden'); + initWorkspace(); + }, 1500); + } else { + setStepFailed(elements.stepDeps); + elements.setupStatusText.innerText = "Setup failed. Check system dependencies."; + elements.btnStartSetup.disabled = false; + showToast("Setup Error", "Failed to build Python environment.", "error"); + } + } catch (err) { + elements.setupStatusText.innerText = `Error: ${err.message}`; + elements.btnStartSetup.disabled = false; + showToast("Setup Error", err.message, "error"); + } +}); + +// --- IMAGE FILE INPUT ENGINE --- +function setupDragAndDrop() { + const dropzone = elements.dropzone; + + ['dragenter', 'dragover'].forEach(eventName => { + dropzone.addEventListener(eventName, (e) => { + e.preventDefault(); + dropzone.classList.add('dragover'); + }, false); + }); + + ['dragleave', 'drop'].forEach(eventName => { + dropzone.addEventListener(eventName, (e) => { + e.preventDefault(); + dropzone.classList.remove('dragover'); + }, false); + }); + + dropzone.addEventListener('drop', (e) => { + const dt = e.dataTransfer; + const files = dt.files; + if (files.length > 0) { + handleImageSelection(files[0].path); + } + }); +} + +async function selectLocalImage() { + const filePath = await window.api.selectImage(); + if (filePath) { + handleImageSelection(filePath); + } +} + +function handleImageSelection(filePath) { + currentImagePath = filePath; + originalFileName = filePath.split(/[\\/]/).pop(); + + // Reset previous states + processedTempPath = null; + activeBgMode = 'transparent'; + scaleX = 1; + scaleY = 1; + resetBgSettings(); + + // Show image in preview element + elements.imageElement.src = `file://${filePath}`; + + // Set file metadata in control tower + const stats = getFileStats(filePath); + elements.infoFilename.innerText = originalFileName; + elements.infoSize.innerText = stats.sizeFormatted; + + // Load dimensions + const img = new Image(); + img.onload = function() { + elements.infoResolution.innerText = `${this.naturalWidth} x ${this.naturalHeight} px`; + + // Switch views + elements.dropzone.classList.add('hidden'); + elements.editorContainer.classList.remove('hidden'); + elements.infoCardEmpty.classList.add('hidden'); + elements.infoCardLoaded.classList.remove('hidden'); + + // Enable AI button + elements.btnRemoveBg.disabled = false; + + // Initialize CropperJS on image + initCropper(); + }; + img.src = `file://${filePath}`; +} + +function getFileStats(path) { + // Simple synchronous-like wrapper or mockup stats. + // Note: Electron file handles path directly. + return { + sizeFormatted: "Calculating..." // Updated as soon as loaded + }; +} + +// Helper: Formatter +function formatBytes(bytes, decimals = 2) { + if (bytes === 0) return '0 Bytes'; + const k = 1024; + const dm = decimals < 0 ? 0 : decimals; + const sizes = ['Bytes', 'KB', 'MB', 'GB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]; +} + +// --- CROPPERJS CONTROLLER HUB --- +function initCropper() { + if (cropper) { + cropper.destroy(); + } + + // Load Cropper configuration + cropper = new Cropper(elements.imageElement, { + viewMode: 1, + dragMode: 'move', + autoCrop: false, + responsive: true, + restore: false, + guides: true, + center: true, + highlight: false, + cropBoxMovable: true, + cropBoxResizable: true, + toggleDragModeOnDblclick: false, + }); + + // Wire toolbar click listeners + elements.ctrlCropDrag.classList.add('active'); + elements.ctrlCropBox.classList.remove('active'); + elements.ctrlAutoTrim.disabled = true; // Disabled until background is removed +} + +function setupToolbarListeners() { + elements.ctrlCropDrag.addEventListener('click', () => { + cropper.setDragMode('move'); + elements.ctrlCropDrag.classList.add('active'); + elements.ctrlCropBox.classList.remove('active'); + }); + + elements.ctrlCropBox.addEventListener('click', () => { + cropper.setDragMode('crop'); + elements.ctrlCropDrag.classList.remove('active'); + elements.ctrlCropBox.classList.add('active'); + }); + + elements.ctrlRotL.addEventListener('click', () => cropper.rotate(-90)); + elements.ctrlRotR.addEventListener('click', () => cropper.rotate(90)); + + elements.ctrlFlipH.addEventListener('click', () => { + scaleX = scaleX === 1 ? -1 : 1; + cropper.scale(scaleX, scaleY); + }); + + elements.ctrlFlipV.addEventListener('click', () => { + scaleY = scaleY === 1 ? -1 : 1; + cropper.scale(scaleX, scaleY); + }); + + elements.cropRatioSelect.addEventListener('change', (e) => { + const value = parseFloat(e.target.value); + cropper.setAspectRatio(value); + }); + + elements.ctrlReset.addEventListener('click', () => { + cropper.reset(); + scaleX = 1; + scaleY = 1; + cropper.scale(1, 1); + elements.cropRatioSelect.value = "NaN"; + cropper.setAspectRatio(NaN); + showToast("Reset Action", "Workspace state cleared.", "info"); + }); + + elements.ctrlAutoTrim.addEventListener('click', autoTrimTransparent); +} + +// --- NATIVE AUTOTRIM (PIXEL ITERATION BOUNDS) --- +function autoTrimTransparent() { + if (!cropper || !processedTempPath) return; + + // We request canvas size matching the full uncropped image dimension + const canvas = cropper.getCroppedCanvas({ + width: cropper.getImageData().naturalWidth, + height: cropper.getImageData().naturalHeight + }); + + if (!canvas) { + showToast("Trim Warning", "Could not analyze transparent boundaries.", "error"); + return; + } + + const ctx = canvas.getContext('2d'); + const imgData = ctx.getImageData(0, 0, canvas.width, canvas.height); + const data = imgData.data; + + let minX = canvas.width; + let maxX = 0; + let minY = canvas.height; + let maxY = 0; + let foundAlpha = false; + + // Loop through pixels in grid to find bounding box of non-transparent values + for (let y = 0; y < canvas.height; y++) { + for (let x = 0; x < canvas.width; x++) { + const alphaIndex = (y * canvas.width + x) * 4 + 3; + if (data[alphaIndex] > 8) { // Alpha transparency cut-off + if (x < minX) minX = x; + if (x > maxX) maxX = x; + if (y < minY) minY = y; + if (y > maxY) maxY = y; + foundAlpha = true; + } + } + } + + if (foundAlpha) { + // Add small padding to avoid clipping exactly + const pad = 10; + minX = Math.max(0, minX - pad); + minY = Math.max(0, minY - pad); + maxX = Math.min(canvas.width, maxX + pad); + maxY = Math.min(canvas.height, maxY + pad); + + // Convert pixel coordinates to Cropper canvas boundaries + const canvasData = cropper.getCanvasData(); + const scale = canvasData.width / cropper.getImageData().naturalWidth; + + // Set crop boundaries + cropper.setCropBoxData({ + left: canvasData.left + minX * scale, + top: canvasData.top + minY * scale, + width: (maxX - minX) * scale, + height: (maxY - minY) * scale + }); + + // Switch to crop selection mode visual indicator + cropper.setDragMode('crop'); + elements.ctrlCropDrag.classList.remove('active'); + elements.ctrlCropBox.classList.add('active'); + + showToast("Auto-Trim Complete", "Crop boundaries adjusted securely around subject.", "success"); + } else { + showToast("Trim Info", "No transparent border boundaries identified.", "info"); + } +} + +// --- CONTROL TOWER CONTROLLER --- +function setupControlTowerListeners() { + // Remove Background click + elements.btnRemoveBg.addEventListener('click', executeBackgroundRemoval); + + // Background Settings Tabs + elements.tabBgTrans.addEventListener('click', () => switchBgMode('transparent')); + elements.tabBgSolid.addEventListener('click', () => switchBgMode('solid')); + elements.tabBgGrad.addEventListener('click', () => switchBgMode('gradient')); + + // Export Quality Slider + elements.exportFormat.addEventListener('change', (e) => { + if (e.target.value === 'png') { + elements.rowQualitySlider.classList.add('hidden'); + } else { + elements.rowQualitySlider.classList.remove('hidden'); + } + }); + + elements.exportQuality.addEventListener('input', (e) => { + elements.exportQualityVal.innerText = `${e.target.value}%`; + }); + + // Export MASTERPIECE Click + elements.btnExport.addEventListener('click', executeExport); +} + +// --- BIREFNET EXECUTION PIPELINE --- +async function executeBackgroundRemoval() { + if (!currentImagePath) return; + + elements.btnRemoveBg.classList.add('hidden'); + elements.loaderBox.classList.remove('hidden'); + elements.loaderText.innerText = "Initializing AI model..."; + elements.loaderSubtext.innerText = "Loading neural weights into memory..."; + + // Get unique temporary output file path + const tempOut = await window.api.getTempPath('transparent_result.png'); + + // Listen to execution progress + window.api.onProcessProgress((progress) => { + if (progress.includes("[STATUS]")) { + const statusText = progress.replace("[STATUS] ", ""); + elements.loaderText.innerText = statusText; + + if (statusText.includes("Hugging Face")) { + elements.loaderSubtext.innerText = "This downloads ~200MB of weights on first run."; + } else if (statusText.includes("inference")) { + elements.loaderSubtext.innerText = "Extracting details and soft matting alpha..."; + } else if (statusText.includes("Saving")) { + elements.loaderSubtext.innerText = "Assembling high-res RGBA container..."; + } + } + }); + + try { + const result = await window.api.removeBackground(currentImagePath, tempOut); + + if (result.success) { + processedTempPath = result.outputPath; + showToast("AI Success", "Image background removed successfully!", "success"); + + // Update workspace preview image source to output transparent PNG + // Add timestamp to bypass browser cache refreshing + const freshSrc = `file://${processedTempPath}?t=${Date.now()}`; + + // Re-initialize CropperJS on the new image src + elements.imageElement.src = freshSrc; + + // Wait for image source update to build the cropper safely + setTimeout(() => { + initCropper(); + + // Unlock background customization and export + elements.secCanvasBg.classList.remove('disabled-group'); + elements.secExport.classList.remove('disabled-group'); + elements.ctrlAutoTrim.disabled = false; // Unlock trim feature! + }, 300); + + } else { + showToast("Inference Error", result.error || "Execution terminated unexpectedly.", "error"); + } + } catch (err) { + showToast("AI Fatal Error", err.message, "error"); + } finally { + elements.loaderBox.classList.add('hidden'); + elements.btnRemoveBg.classList.remove('hidden'); + } +} + +// --- CANVAS CUSTOM BACKGROUND SETTINGS --- +function resetBgSettings() { + elements.secCanvasBg.classList.add('disabled-group'); + elements.secExport.classList.add('disabled-group'); + switchBgMode('transparent'); +} + +function switchBgMode(mode) { + activeBgMode = mode; + + // Adjust active tab css + elements.tabBgTrans.classList.remove('active'); + elements.tabBgSolid.classList.remove('active'); + elements.tabBgGrad.classList.remove('active'); + + elements.subpanelSolid.classList.add('hidden'); + elements.subpanelGradient.classList.add('hidden'); + + if (mode === 'transparent') { + elements.tabBgTrans.classList.add('active'); + elements.canvasBgOverlay.style.opacity = '0'; + } else if (mode === 'solid') { + elements.tabBgSolid.classList.add('active'); + elements.subpanelSolid.classList.remove('hidden'); + elements.canvasBgOverlay.style.background = activeSolidColor; + elements.canvasBgOverlay.style.opacity = '1'; + } else if (mode === 'gradient') { + elements.tabBgGrad.classList.add('active'); + elements.subpanelGradient.classList.remove('hidden'); + + // Draw current active gradient stops onto the workspace preview + const cssGrad = buildCssGradientString(activeGradientStops); + elements.canvasBgOverlay.style.background = cssGrad; + elements.canvasBgOverlay.style.opacity = '1'; + } +} + +function buildCssGradientString(stops) { + return `linear-gradient(135deg, ${stops[0].color}, ${stops[1].color})`; +} + +function setupBgSwatches() { + // Solid Swatches + const solidSwatches = document.querySelectorAll('.swatch:not(.custom)'); + solidSwatches.forEach(swatch => { + swatch.addEventListener('click', (e) => { + solidSwatches.forEach(s => s.classList.remove('active')); + e.target.classList.add('active'); + + activeSolidColor = e.target.getAttribute('data-color'); + elements.canvasBgOverlay.style.background = activeSolidColor; + }); + }); + + // Custom Color Picker + elements.customColorPicker.addEventListener('input', (e) => { + activeSolidColor = e.target.value; + elements.canvasBgOverlay.style.background = activeSolidColor; + + // Highlight custom swatch borders + elements.btnCustomColorPicker.classList.add('active'); + solidSwatches.forEach(s => s.classList.remove('active')); + }); + + // Gradient Swatches + const gradSwatches = document.querySelectorAll('.grad-swatch'); + gradSwatches.forEach(swatch => { + swatch.addEventListener('click', (e) => { + gradSwatches.forEach(s => s.classList.remove('active')); + e.target.classList.add('active'); + + const gradKey = e.target.className.split(' ').find(cls => cls.startsWith('grad-')); + if (GRADIENTS[gradKey]) { + activeGradientStops = GRADIENTS[gradKey]; + elements.canvasBgOverlay.style.background = e.target.getAttribute('data-gradient'); + } + }); + }); +} + +// --- CANVAS EXPORT AND MERGING ENGINE --- +async function executeExport() { + if (!cropper) return; + + elements.btnExport.disabled = true; + elements.btnExport.innerHTML = ` + + Exporting masterpiece... + `; + + setTimeout(async () => { + try { + // Get the cropped canvas at original resolution + const croppedCanvas = cropper.getCroppedCanvas(); + + if (!croppedCanvas) { + throw new Error("Failed to extract active cropped canvas workspace."); + } + + let finalCanvas = croppedCanvas; + + // If solid or gradient background mode is selected, we burn it behind the subject + if (activeBgMode !== 'transparent') { + finalCanvas = document.createElement('canvas'); + finalCanvas.width = croppedCanvas.width; + finalCanvas.height = croppedCanvas.height; + const ctx = finalCanvas.getContext('2d'); + + if (activeBgMode === 'solid') { + ctx.fillStyle = activeSolidColor; + ctx.fillRect(0, 0, finalCanvas.width, finalCanvas.height); + } else if (activeBgMode === 'gradient') { + // Render the 135deg gradient vector on final output canvas + const grad = ctx.createLinearGradient(0, 0, finalCanvas.width, finalCanvas.height); + activeGradientStops.forEach(stop => { + grad.addColorStop(stop.offset, stop.color); + }); + ctx.fillStyle = grad; + ctx.fillRect(0, 0, finalCanvas.width, finalCanvas.height); + } + + // Overlay transparent subject on top of custom background + ctx.drawImage(croppedCanvas, 0, 0); + } + + // Read Export Format settings + const format = elements.exportFormat.value; + let mimeType = 'image/png'; + let extension = 'png'; + + if (format === 'jpeg') { + mimeType = 'image/jpeg'; + extension = 'jpg'; + } else if (format === 'webp') { + mimeType = 'image/webp'; + extension = 'webp'; + } + + const quality = parseFloat(elements.exportQuality.value) / 100; + const base64Data = finalCanvas.toDataURL(mimeType, quality); + + // Create default filename suggestion + const nameWithoutExt = originalFileName.substring(0, originalFileName.lastIndexOf('.')) || originalFileName; + const exportDefaultName = `${nameWithoutExt}_no_bg.${extension}`; + + // Open Save File dialog via context bridge + const result = await window.api.exportImage(base64Data, exportDefaultName); + + if (result.success) { + showToastWithAction( + "Export Successful", + `Saved file correctly: ${result.filePath.split(/[\\/]/).pop()}`, + "success", + result.filePath + ); + } else if (!result.canceled) { + showToast("Export Failed", result.error || "Save operation failed.", "error"); + } + } catch (err) { + showToast("Export Error", err.message, "error"); + } finally { + // Re-enable button + elements.btnExport.disabled = false; + elements.btnExport.innerHTML = ` + + Export Masterpiece + `; + } + }, 100); +} + +// --- GLOBAL TOAST SYSTEM IMPLEMENTATION --- +let toastTimeout = null; + +function setupToastListeners() { + elements.btnCloseToast.addEventListener('click', hideToast); +} + +function showToast(title, message, type = 'info') { + hideToast(); + + elements.toastTitle.innerText = title; + elements.toastMessage.innerText = message; + + // Styling + elements.toastBanner.className = 'toast-notification glass-panel-inner'; + if (type === 'success') elements.toastBanner.classList.add('success'); + if (type === 'error') elements.toastBanner.classList.add('error'); + + elements.toastBanner.classList.remove('hidden'); + + // Set automatic dismissal + toastTimeout = setTimeout(hideToast, 5000); +} + +function showToastWithAction(title, message, type = 'success', filePath) { + hideToast(); + + elements.toastTitle.innerText = title; + elements.toastMessage.innerHTML = `${message}
`; + + elements.toastBanner.className = 'toast-notification glass-panel-inner'; + if (type === 'success') elements.toastBanner.classList.add('success'); + if (type === 'error') elements.toastBanner.classList.add('error'); + + elements.toastBanner.classList.remove('hidden'); + + // Bind click logic to "Show in File Explorer" button + setTimeout(() => { + const btnExplore = document.getElementById('btn-toast-explore'); + if (btnExplore) { + btnExplore.addEventListener('click', () => { + window.api.showInExplorer(filePath); + }); + } + }, 50); + + // Keep it active longer since it contains interaction + toastTimeout = setTimeout(hideToast, 8000); +} + +function hideToast() { + if (toastTimeout) { + clearTimeout(toastTimeout); + toastTimeout = null; + } + elements.toastBanner.classList.add('hidden'); +} diff --git a/styles.css b/styles.css new file mode 100644 index 0000000..dbca5ae --- /dev/null +++ b/styles.css @@ -0,0 +1,1134 @@ +/* --- DESIGN SYSTEM & CSS VARIABLES --- */ +:root { + --font-heading: 'Outfit', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + --font-body: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + + /* Harmonious Dark / Neon Palette */ + --bg-dark: #0a0a0f; + --panel-bg: rgba(16, 16, 24, 0.65); + --panel-bg-inner: rgba(255, 255, 255, 0.03); + --panel-border: rgba(255, 255, 255, 0.08); + --panel-border-inner: rgba(255, 255, 255, 0.04); + + --text-main: #f3f4f6; + --text-muted: #9ca3af; + --text-muted-dark: #6b7280; + + /* Neon Accents - Turunçgil Orange & Gold Palette */ + --accent-purple: #ff7a00; + --accent-purple-glow: rgba(255, 122, 0, 0.35); + --accent-cyan: #ffb800; + --accent-cyan-glow: rgba(255, 184, 0, 0.3); + --accent-pink: #f97316; + + --danger: #ef4444; + --danger-glow: rgba(239, 68, 68, 0.3); + --success: #10b981; +} + +/* --- BASE STYLING --- */ +* { + box-sizing: border-box; + margin: 0; + padding: 0; + user-select: none; +} + +body { + font-family: var(--font-body); + background-color: var(--bg-dark); + color: var(--text-main); + height: 100vh; + overflow: hidden; + position: relative; + display: flex; + flex-direction: column; +} + +/* --- AMBIENT GLOW ORBS --- */ +.ambient-glow { + position: absolute; + width: 45vw; + height: 45vw; + border-radius: 50%; + filter: blur(160px); + opacity: 0.28; + z-index: -1; + pointer-events: none; +} + +.bg-orb-1 { + background: var(--accent-purple); + top: -15%; + left: -15%; + animation: orb-pulse 16s ease-in-out infinite alternate; +} + +.bg-orb-2 { + background: var(--accent-cyan); + bottom: -15%; + right: -15%; + animation: orb-pulse 12s ease-in-out infinite alternate-reverse; +} + +@keyframes orb-pulse { + 0% { transform: scale(1) translate(0, 0); opacity: 0.25; } + 50% { transform: scale(1.15) translate(30px, -50px); opacity: 0.33; } + 100% { transform: scale(0.9) translate(-20px, 30px); opacity: 0.22; } +} + +/* --- SLEEK FRAMELESS TITLEBAR --- */ +.app-titlebar { + height: 44px; + background: rgba(10, 10, 15, 0.55); + backdrop-filter: blur(12px); + border-bottom: 1px solid var(--panel-border); + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 16px; + -webkit-app-region: drag; + z-index: 1000; + flex-shrink: 0; +} + +.titlebar-logo { + display: flex; + align-items: center; + gap: 10px; + font-family: var(--font-heading); + font-weight: 700; + letter-spacing: 0.5px; + font-size: 14px; + color: #fff; + text-shadow: 0 0 12px rgba(139, 92, 246, 0.4); +} + +.titlebar-logo .badge { + background: linear-gradient(135deg, var(--accent-purple), var(--accent-cyan)); + font-size: 9px; + font-weight: 800; + padding: 2.5px 6.5px; + border-radius: 50px; + letter-spacing: 0.8px; + text-transform: uppercase; + color: #fff; + border: 1px solid rgba(255, 255, 255, 0.15); +} + +.titlebar-controls { + display: flex; + align-items: center; + height: 100%; + -webkit-app-region: no-drag; +} + +.win-btn { + background: transparent; + border: none; + color: var(--text-muted); + width: 44px; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.2s ease; + cursor: pointer; +} + +.win-btn:hover { + background: rgba(255, 255, 255, 0.05); + color: #fff; +} + +.close-btn:hover { + background: var(--danger); + color: #fff; +} + +/* --- MAIN WORKSPACE GRID --- */ +.app-workspace { + flex: 1; + display: grid; + grid-template-columns: 1fr 340px; + gap: 20px; + padding: 20px; + height: calc(100vh - 44px); + z-index: 10; + overflow: hidden; +} + +@media (max-width: 950px) { + .app-workspace { + grid-template-columns: 1fr; + } + .control-tower { + display: none; /* Just simple responsive check */ + } +} + +/* --- GLASS CONTAINER BASE --- */ +.glass-panel { + background: var(--panel-bg); + backdrop-filter: blur(28px) saturate(190%); + border: 1px solid var(--panel-border); + border-radius: 20px; + box-shadow: 0 16px 45px rgba(0, 0, 0, 0.45), inset 0 1px 0 rgba(255, 255, 255, 0.05); + display: flex; + flex-direction: column; + overflow: hidden; + transition: all 0.3s ease; +} + +.glass-panel-inner { + background: var(--panel-bg-inner); + border: 1px solid var(--panel-border-inner); + border-radius: 12px; + padding: 14px; +} + +/* --- VIEWPORT / SCREEN --- */ +.workspace-viewport { + flex: 1; + position: relative; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +} + +/* --- DROPZONE / EMPTY STATE --- */ +.dropzone { + display: flex; + align-items: center; + justify-content: center; + width: calc(100% - 40px); + height: calc(100% - 40px); + border: 2px dashed rgba(255, 255, 255, 0.12); + border-radius: 16px; + background: rgba(255, 255, 255, 0.01); + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + margin: 20px; + cursor: pointer; +} + +.dropzone:hover, .dropzone.dragover { + border-color: var(--accent-purple); + background: rgba(139, 92, 246, 0.04); + box-shadow: 0 0 35px rgba(139, 92, 246, 0.1); + transform: scale(1.005); +} + +.dropzone-content { + text-align: center; + display: flex; + flex-direction: column; + align-items: center; +} + +.dropzone-glow-icon { + width: 90px; + height: 90px; + border-radius: 50%; + background: rgba(255, 255, 255, 0.02); + border: 1px solid rgba(255, 255, 255, 0.05); + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 24px; + color: var(--text-muted); + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2); + transition: all 0.3s ease; +} + +.dropzone:hover .dropzone-glow-icon { + color: var(--accent-cyan); + border-color: rgba(6, 182, 212, 0.25); + box-shadow: 0 0 30px var(--accent-cyan-glow); + transform: translateY(-5px); +} + +.dropzone h2 { + font-family: var(--font-heading); + font-size: 22px; + font-weight: 600; + margin-bottom: 8px; + color: #fff; + letter-spacing: -0.3px; +} + +.dropzone-sub { + color: var(--text-muted); + font-size: 14px; + margin-bottom: 24px; +} + +.or-separator { + display: flex; + align-items: center; + width: 180px; + margin-bottom: 24px; + color: var(--text-muted-dark); + font-size: 11px; + font-weight: 600; + letter-spacing: 1px; +} + +.or-separator::before, .or-separator::after { + content: ''; + flex: 1; + height: 1px; + background: rgba(255, 255, 255, 0.05); +} + +.or-separator span { + padding: 0 10px; +} + +/* --- EDITOR & CROPPING WORKSPACE --- */ +.editor-container { + display: flex; + flex-direction: column; + width: 100%; + height: 100%; + overflow: hidden; + padding: 20px; +} + +.preview-canvas-wrapper { + flex: 1; + border-radius: 12px; + border: 1px solid var(--panel-border); + overflow: hidden; + display: flex; + align-items: center; + justify-content: center; + position: relative; + background-color: #121216; + box-shadow: inset 0 4px 20px rgba(0,0,0,0.5); +} + +/* Stunning Checkerboard Transparency Styling */ +.checkerboard { + background-image: + linear-gradient(45deg, rgba(255,255,255,0.03) 25%, transparent 25%), + linear-gradient(-45deg, rgba(255,255,255,0.03) 25%, transparent 25%), + linear-gradient(45deg, transparent 75%, rgba(255,255,255,0.03) 75%), + linear-gradient(-45deg, transparent 75%, rgba(255,255,255,0.03) 75%); + background-size: 20px 20px; + background-position: 0 0, 0 10px, 10px -10px, -10px 0px; +} + +.preview-canvas-wrapper img { + max-width: 100%; + max-height: 100%; + object-fit: contain; + display: block; +} + +/* Realtime Background Customized Overlay */ +.canvas-ambient-backdrop { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 1; + pointer-events: none; + opacity: 0; + transition: opacity 0.3s ease; +} + +/* CropperJS Custom Visual Styling overrides */ +.cropper-view-box { + outline: 2px solid var(--accent-purple); + outline-color: var(--accent-purple); +} + +.cropper-line { + background-color: var(--accent-purple); +} + +.cropper-point { + background-color: #fff; + border: 2px solid var(--accent-purple); + width: 8px; + height: 8px; + border-radius: 50%; +} + +.cropper-bg { + background-image: none !important; + background-color: transparent !important; +} + +/* Floating Crop Toolbar Controls */ +.editor-controls { + height: 56px; + margin-top: 16px; + display: flex; + align-items: center; + justify-content: center; + gap: 10px; + padding: 0 16px !important; +} + +.divider-v { + width: 1px; + height: 24px; + background: rgba(255, 255, 255, 0.08); +} + +.btn-group { + display: flex; + gap: 5px; +} + +.ctrl-btn { + background: transparent; + border: 1px solid rgba(255, 255, 255, 0.05); + color: var(--text-muted); + width: 34px; + height: 34px; + border-radius: 8px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); +} + +.ctrl-btn:hover { + background: rgba(255, 255, 255, 0.05); + color: #fff; + border-color: rgba(255, 255, 255, 0.12); +} + +.ctrl-btn.active { + background: var(--accent-purple); + color: #fff; + border-color: var(--accent-purple); + box-shadow: 0 0 15px var(--accent-purple-glow); +} + +.btn-danger-icon:hover { + background: rgba(239, 68, 68, 0.1) !important; + color: var(--danger) !important; + border-color: rgba(239, 68, 68, 0.2) !important; +} + +/* --- CONTROL TOWER (RIGHT SIDE) --- */ +.control-tower { + width: 340px; + padding: 24px; + gap: 24px; + overflow-y: auto; + flex-shrink: 0; + border-left: 1px solid var(--panel-border); +} + +/* Custom Scrollbar for Right Tower */ +.control-tower::-webkit-scrollbar { + width: 6px; +} + +.control-tower::-webkit-scrollbar-track { + background: transparent; +} + +.control-tower::-webkit-scrollbar-thumb { + background: rgba(255, 255, 255, 0.06); + border-radius: 10px; +} + +.control-tower::-webkit-scrollbar-thumb:hover { + background: rgba(255, 255, 255, 0.12); +} + +.tower-group { + display: flex; + flex-direction: column; + gap: 14px; + transition: opacity 0.3s ease; +} + +.disabled-group { + opacity: 0.35; + pointer-events: none; +} + +.tower-heading { + display: flex; + align-items: center; + gap: 10px; +} + +.step-badge { + background: rgba(255, 255, 255, 0.06); + border: 1px solid rgba(255, 255, 255, 0.1); + color: #fff; + width: 24px; + height: 24px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-family: var(--font-heading); + font-size: 11px; + font-weight: 700; +} + +.tower-group:not(.disabled-group) .step-badge { + background: linear-gradient(135deg, var(--accent-purple), var(--accent-cyan)); + border-color: rgba(255,255,255,0.1); + box-shadow: 0 0 10px rgba(139, 92, 246, 0.3); +} + +.tower-heading h3 { + font-family: var(--font-heading); + font-size: 15px; + font-weight: 600; + letter-spacing: -0.2px; + color: #fff; +} + +/* --- INFO CARD --- */ +.info-card { + font-size: 13px; +} + +.info-row { + display: flex; + justify-content: space-between; + padding: 5px 0; + border-bottom: 1px solid rgba(255, 255, 255, 0.02); +} + +.info-row:last-child { + border-bottom: none; +} + +.info-row .label { + color: var(--text-muted); +} + +.info-row .val { + color: #fff; + font-weight: 500; +} + +.truncate { + max-width: 160px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +/* --- BUTTONS --- */ +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 8px; + border-radius: 10px; + font-family: var(--font-body); + font-size: 14px; + font-weight: 600; + height: 42px; + padding: 0 20px; + cursor: pointer; + transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); + border: none; + outline: none; +} + +.btn-primary { + background: linear-gradient(135deg, var(--accent-purple), var(--accent-cyan)); + color: #fff; + box-shadow: 0 4px 20px rgba(139, 92, 246, 0.2); +} + +.btn-primary:hover:not(:disabled) { + box-shadow: 0 0 25px rgba(139, 92, 246, 0.45); + transform: translateY(-1.5px); +} + +.btn-primary:active:not(:disabled) { + transform: translateY(0) scale(0.98); +} + +.btn-primary:disabled { + background: rgba(255, 255, 255, 0.05); + color: var(--text-muted-dark); + box-shadow: none; + cursor: not-allowed; +} + +.glow-btn-purple { + border: 1px solid rgba(139, 92, 246, 0.3) !important; + color: #fff !important; +} + +.glow-btn-purple:hover:not(:disabled) { + background: rgba(139, 92, 246, 0.15) !important; + box-shadow: 0 0 20px rgba(139, 92, 246, 0.25); + border-color: var(--accent-purple) !important; +} + +.glow-btn-cyan { + background: linear-gradient(135deg, var(--accent-cyan), #0891b2) !important; + box-shadow: 0 4px 20px rgba(6, 182, 212, 0.2) !important; +} + +.glow-btn-cyan:hover { + box-shadow: 0 0 25px rgba(6, 182, 212, 0.45) !important; +} + +.btn-secondary { + background: rgba(255, 255, 255, 0.04); + border: 1px solid rgba(255, 255, 255, 0.07); + color: var(--text-main); +} + +.btn-secondary:hover:not(:disabled) { + background: rgba(255, 255, 255, 0.08); + border-color: rgba(255, 255, 255, 0.15); +} + +.btn-glass { + background: rgba(255, 255, 255, 0.04); + border: 1px solid rgba(255, 255, 255, 0.08); + backdrop-filter: blur(10px); + color: #fff; +} + +.btn-glass:hover { + background: rgba(255, 255, 255, 0.08); + border-color: rgba(255, 255, 255, 0.15); + transform: translateY(-1px); +} + +.btn-sm { + height: 34px; + font-size: 12.5px; + padding: 0 12px; +} + +.w-full { + width: 100%; +} + +/* --- LOADER BOX (PROCESSING STATE) --- */ +.loader-box { + display: flex; + flex-direction: column; + align-items: center; + padding: 20px !important; + gap: 16px; + text-align: center; + animation: fade-in 0.3s ease; +} + +.loader-ring-wrapper { + position: relative; + width: 48px; + height: 48px; +} + +.glow-ring { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + border-radius: 50%; + border: 3px solid transparent; + border-top-color: var(--accent-purple); + border-bottom-color: var(--accent-cyan); + filter: blur(8px); + animation: spin 1s linear infinite; + opacity: 0.75; +} + +.loader-ring { + width: 100%; + height: 100%; + animation: spin 1s linear infinite; +} + +.loader-ring circle { + stroke: url(#neon-grad); + stroke-linecap: round; + animation: dash 1.5s ease-in-out infinite; +} + +@keyframes spin { + 100% { transform: rotate(360deg); } +} + +@keyframes dash { + 0% { stroke-dasharray: 1, 150; stroke-dashoffset: 0; } + 50% { stroke-dasharray: 90, 150; stroke-dashoffset: -35; } + 100% { stroke-dasharray: 90, 150; stroke-dashoffset: -124; } +} + +.loader-text { + font-weight: 600; + font-size: 13.5px; + color: #fff; +} + +.loader-subtext { + font-size: 11px; + color: var(--text-muted); +} + +/* --- SELECT ELEMENT --- */ +.select-wrapper { + position: relative; + display: flex; + align-items: center; + width: 100%; +} + +.select-wrapper::after { + content: ''; + position: absolute; + right: 12px; + width: 0; + height: 0; + border-left: 4.5px solid transparent; + border-right: 4.5px solid transparent; + border-top: 5.5px solid var(--text-muted); + pointer-events: none; +} + +select { + appearance: none; + background: rgba(255, 255, 255, 0.03); + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: 8px; + color: #fff; + padding: 8px 30px 8px 12px; + width: 100%; + font-family: var(--font-body); + font-size: 13px; + font-weight: 500; + outline: none; + cursor: pointer; + transition: all 0.2s ease; +} + +select:hover { + background: rgba(255, 255, 255, 0.06); + border-color: rgba(255, 255, 255, 0.15); +} + +select option { + background: #14141c; + color: #fff; +} + +/* --- BACKGROUND SETTINGS CUSTOMIZER --- */ +.bg-mode-tabs { + display: grid; + grid-template-columns: repeat(3, 1fr); + padding: 4px !important; + gap: 2px; +} + +.bg-tab { + background: transparent; + border: none; + color: var(--text-muted); + padding: 6.5px 0; + border-radius: 7px; + font-family: var(--font-body); + font-size: 11.5px; + font-weight: 600; + cursor: pointer; + transition: all 0.2s ease; +} + +.bg-tab:hover { + color: #fff; +} + +.bg-tab.active { + background: rgba(255, 255, 255, 0.06); + color: #fff; + border: 1px solid rgba(255, 255, 255, 0.04); +} + +.bg-settings-subpanel { + margin-top: 10px; + display: flex; + flex-direction: column; + gap: 8px; + animation: slide-down 0.25s ease; +} + +.panel-sublabel { + font-size: 11px; + font-weight: 600; + color: var(--text-muted-dark); + text-transform: uppercase; + letter-spacing: 0.6px; +} + +.color-swatches-grid, .gradient-swatches-grid { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.swatch, .grad-swatch { + width: 28px; + height: 28px; + border-radius: 6px; + border: 2px solid transparent; + cursor: pointer; + transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); + box-shadow: 0 4px 10px rgba(0,0,0,0.15); +} + +.swatch:hover, .grad-swatch:hover { + transform: scale(1.15); + z-index: 2; +} + +.swatch.active, .grad-swatch.active { + border-color: #fff; + box-shadow: 0 0 15px rgba(255, 255, 255, 0.3), 0 4px 10px rgba(0,0,0,0.3); + transform: scale(1.1); +} + +/* Swatches */ +.swatch.white { background: #ffffff; } +.swatch.black { background: #000000; } +.swatch.red { background: #ef4444; } +.swatch.blue { background: #3b82f6; } +.swatch.green { background: #10b981; } + +.swatch.custom { + background: rgba(255, 255, 255, 0.04); + border: 1px solid rgba(255, 255, 255, 0.08); + display: flex; + align-items: center; + justify-content: center; + color: var(--text-muted); + position: relative; + overflow: hidden; +} + +.swatch.custom:hover { + color: #fff; +} + +.swatch.custom input[type="color"] { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + opacity: 0; + cursor: pointer; +} + +/* Premium Gradients */ +.grad-1 { background: linear-gradient(135deg, #a78bfa, #06b6d4); } +.grad-2 { background: linear-gradient(135deg, #f43f5e, #f97316); } +.grad-3 { background: linear-gradient(135deg, #10b981, #06b6d4); } +.grad-4 { background: linear-gradient(135deg, #ec4899, #8b5cf6); } +.grad-5 { background: linear-gradient(135deg, #3b82f6, #8b5cf6); } +.grad-6 { background: linear-gradient(135deg, #f59e0b, #ef4444); } + +/* --- EXPORT STUDIO PANEL --- */ +.export-settings-box { + display: flex; + flex-direction: column; + gap: 12px; +} + +.settings-row { + display: flex; + flex-direction: column; + gap: 6px; +} + +.settings-row label { + font-size: 12.5px; + font-weight: 500; + color: var(--text-muted); +} + +.slider-labels { + display: flex; + justify-content: space-between; + font-size: 12.5px; +} + +.slider-val { + color: #fff; + font-weight: 600; +} + +/* Glass Slider Styling */ +input[type="range"] { + appearance: none; + background: rgba(255,255,255,0.06); + border: 1px solid rgba(255,255,255,0.04); + height: 6px; + border-radius: 5px; + outline: none; + cursor: pointer; + width: 100%; +} + +input[type="range"]::-webkit-slider-thumb { + appearance: none; + width: 16px; + height: 16px; + border-radius: 50%; + background: linear-gradient(135deg, var(--accent-purple), var(--accent-cyan)); + box-shadow: 0 0 10px var(--accent-purple-glow); + cursor: pointer; + transition: transform 0.1s ease; +} + +input[type="range"]::-webkit-slider-thumb:hover { + transform: scale(1.2); +} + +/* --- SETUP OVERLAY SCREEN --- */ +.overlay-container { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 580px; + padding: 40px; + z-index: 10000; + animation: scale-up 0.4s cubic-bezier(0.34, 1.56, 0.64, 1); +} + +.setup-wizard { + display: flex; + flex-direction: column; + gap: 26px; +} + +.glow-icon-container { + display: flex; + justify-content: center; + margin-bottom: 8px; +} + +.glow-icon { + filter: drop-shadow(0 0 20px var(--accent-purple-glow)); +} + +.setup-wizard h1 { + font-family: var(--font-heading); + font-size: 26px; + font-weight: 700; + color: #fff; + letter-spacing: -0.5px; + margin-bottom: 6px; +} + +.subtitle { + color: var(--text-muted); + font-size: 14px; + line-height: 1.5; +} + +.setup-progress-box { + text-align: left; + display: flex; + flex-direction: column; + gap: 16px; +} + +.setup-steps { + display: flex; + flex-direction: column; + gap: 12px; +} + +.step-item { + display: flex; + align-items: center; + gap: 12px; + font-size: 13px; + color: var(--text-muted); + transition: all 0.3s ease; +} + +.step-dot { + width: 8px; + height: 8px; + border-radius: 50%; + background: rgba(255, 255, 255, 0.1); + border: 1px solid rgba(255, 255, 255, 0.05); + display: inline-block; + transition: all 0.3s ease; +} + +.step-item.active { + color: #fff; + font-weight: 500; +} + +.step-item.active .step-dot { + background: var(--accent-purple); + box-shadow: 0 0 10px var(--accent-purple-glow); + transform: scale(1.2); +} + +.step-item.done { + color: var(--success); +} + +.step-item.done .step-dot { + background: var(--success); + box-shadow: 0 0 8px rgba(16, 185, 129, 0.3); +} + +.step-item.failed { + color: var(--danger); +} + +.step-item.failed .step-dot { + background: var(--danger); + box-shadow: 0 0 8px var(--danger-glow); +} + +.progress-bar-container { + height: 6px; + background: rgba(255, 255, 255, 0.04); + border-radius: 50px; + overflow: hidden; + position: relative; + display: none; /* Shows during setup */ +} + +.progress-bar { + height: 100%; + width: 0; + background: linear-gradient(90deg, var(--accent-purple), var(--accent-cyan)); + box-shadow: 0 0 15px var(--accent-purple-glow); + border-radius: 50px; + transition: width 0.3s ease; +} + +.progress-status { + font-size: 11px; + color: var(--text-muted-dark); + text-align: center; +} + +.wizard-actions { + display: flex; + justify-content: center; +} + +/* --- TOAST NOTIFICATIONS --- */ +.toast-notification { + position: absolute; + bottom: 24px; + right: 24px; + width: 320px; + padding: 14px 18px !important; + display: flex; + align-items: flex-start; + gap: 12px; + border-left: 4px solid var(--accent-purple); + z-index: 100000; + animation: slide-in 0.4s cubic-bezier(0.16, 1, 0.3, 1); + box-shadow: 0 20px 40px rgba(0,0,0,0.5); +} + +.toast-notification.success { + border-left-color: var(--success); +} + +.toast-notification.error { + border-left-color: var(--danger); +} + +.toast-icon { + margin-top: 2px; +} + +.toast-content { + flex: 1; +} + +.toast-title { + font-family: var(--font-heading); + font-size: 13.5px; + font-weight: 700; + color: #fff; + margin-bottom: 2px; +} + +.toast-message { + font-size: 12px; + color: var(--text-muted); + line-height: 1.4; +} + +.toast-close { + background: transparent; + border: none; + color: var(--text-muted-dark); + font-size: 18px; + cursor: pointer; + margin-top: -4px; + transition: color 0.2s ease; +} + +.toast-close:hover { + color: #fff; +} + +/* --- ANIMATIONS --- */ +@keyframes fade-in { + from { opacity: 0; } + to { opacity: 1; } +} + +@keyframes slide-down { + from { transform: translateY(-10px); opacity: 0; } + to { transform: translateY(0); opacity: 1; } +} + +@keyframes slide-in { + from { transform: translateX(50px); opacity: 0; } + to { transform: translateX(0); opacity: 1; } +} + +@keyframes scale-up { + from { transform: translate(-50%, -40%) scale(0.95); opacity: 0; } + to { transform: translate(-50%, -50%) scale(1); opacity: 1; } +} + +.animated-float { + animation: float 4s ease-in-out infinite; +} + +@keyframes float { + 0% { transform: translateY(0px); } + 50% { transform: translateY(-8px); } + 100% { transform: translateY(0px); } +} + +/* --- HELPER CLASSES --- */ +.hidden { + display: none !important; +} + +.text-muted { color: var(--text-muted); } +.text-center { text-align: center; } +.py-2 { padding-top: 8px; padding-bottom: 8px; }