diff --git a/overlay.html b/overlay.html index 4db4b80..7879d67 100644 --- a/overlay.html +++ b/overlay.html @@ -5,19 +5,102 @@ CrosshairOverlay diff --git a/package-lock.json b/package-lock.json index cdc4bd8..d0d9929 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "crosshair-overlay", - "version": "0.0.0", + "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "crosshair-overlay", - "version": "0.0.0", + "version": "1.0.0", "dependencies": { "@tauri-apps/plugin-global-shortcut": "^2.3.1", "@tauri-apps/plugin-log": "^2.8.0", diff --git a/package.json b/package.json index d926591..47a4abe 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "crosshair-overlay", "private": true, - "version": "0.0.0", + "version": "1.0.0", "type": "module", "scripts": { "dev": "vite", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 5e6d4b0..9ee2ff1 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "crosshair-overlay" -version = "0.1.0" +version = "1.0.0" description = "Crosshair Overlay for Games" authors = ["you"] license = "MIT" @@ -20,10 +20,18 @@ tauri = { version = "2", features = ["tray-icon", "macos-private-api"] } tauri-plugin-log = "2" tauri-plugin-global-shortcut = "2" tauri-plugin-store = "2" +tauri-plugin-single-instance = "2" serde = { version = "1", features = ["derive"] } serde_json = "1" log = "0.4" tokio = { version = "1", features = ["time"] } [target.'cfg(target_os = "macos")'.dependencies] -cocoa = "0.25" +objc2 = "0.6" +objc2-app-kit = "0.2" +objc2-foundation = "0.3" + +[target.'cfg(target_os = "windows")'.dependencies] +webview2-com = "0.39" +tauri-plugin-single-instance = "2" +winapi = { version = "0.3", features = ["winuser", "libloaderapi", "windef"] } diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index 607860c..d8d2b70 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -5,6 +5,7 @@ "windows": ["main", "crosshair-layer"], "permissions": [ "core:default", + "core:app:default", "core:window:default", "core:window:allow-show", "core:window:allow-hide", @@ -32,6 +33,7 @@ "core:window:allow-available-monitors", "core:window:allow-is-minimized", "core:window:allow-set-resizable", + "core:window:allow-request-user-attention", "core:tray:default", "core:menu:default", "core:event:default", diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 8ee4d04..487247d 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -55,8 +55,7 @@ impl Default for CrosshairConfig { } /// Create the transparent overlay window for the crosshair display. -#[tauri::command] -async fn create_crosshair_window(app: AppHandle) -> Result<(), String> { +async fn create_crosshair_window_internal(app: AppHandle) -> Result<(), String> { if CROSSHAIR_WINDOW_CREATED.swap(true, Ordering::SeqCst) { return Ok(()); } @@ -67,14 +66,13 @@ async fn create_crosshair_window(app: AppHandle) -> Result<(), String> { WebviewUrl::App("overlay.html".into()), ) .title("CrosshairOverlay") - .fullscreen(true) .decorations(false) .transparent(true) .always_on_top(true) .skip_taskbar(true) .resizable(false) .focused(false) - .visible(true) + .visible(true) // Start as visible to ensure it shows up .build() .map_err(|e: tauri::Error| e.to_string())?; @@ -82,9 +80,151 @@ async fn create_crosshair_window(app: AppHandle) -> Result<(), String> { .set_ignore_cursor_events(true) .map_err(|e: tauri::Error| e.to_string())?; + // macOS: use fullscreen for best transparency support + #[cfg(target_os = "macos")] + { + overlay.set_fullscreen(true).map_err(|e: tauri::Error| e.to_string())?; + } + + // Non-macOS: position window to cover primary monitor + #[cfg(not(target_os = "macos"))] + { + use tauri::{PhysicalPosition, PhysicalSize}; + if let Ok(monitor) = overlay.current_monitor() { + if let Some(monitor) = monitor { + let size = monitor.size(); + let pos = monitor.position(); + let _ = overlay.set_position(PhysicalPosition::new(pos.x, pos.y)); + let _ = overlay.set_size(PhysicalSize::new(size.width, size.height)); + } + } + } + + // Set window background to transparent on macOS using objc2 + #[cfg(target_os = "macos")] + { + use objc2::{msg_send, class}; + + let overlay_clone = overlay.clone(); + std::thread::spawn(move || { + std::thread::sleep(std::time::Duration::from_millis(300)); + unsafe { + if let Ok(ns_window_ptr) = overlay_clone.ns_window() { + let ns_window = ns_window_ptr as *mut objc2::runtime::AnyObject; + if !ns_window.is_null() { + let _: () = msg_send![ns_window, setOpaque: false as bool]; + let clear_color: *mut objc2::runtime::AnyObject = msg_send![class!(NSColor), clearColor]; + if !clear_color.is_null() { + let _: () = msg_send![ns_window, setBackgroundColor: clear_color]; + } + let _: () = msg_send![ns_window, display]; + } + } + } + }); + } + + // Windows: Set WebView2 background to transparent using webview2-com + #[cfg(target_os = "windows")] + { + // Use a simple approach to force WebView2 transparency + let overlay_clone = overlay.clone(); + std::thread::spawn(move || { + std::thread::sleep(std::time::Duration::from_millis(1000)); + + // Try to set window transparency using Windows API + if let Ok(hwnd) = overlay_clone.hwnd() { + unsafe { + let hwnd_raw = hwnd.0 as isize; + + // Remove window borders and force transparency + let user32 = winapi::um::libloaderapi::GetModuleHandleA(b"user32\0".as_ptr() as *const i8); + if !user32.is_null() { + // Set window to layered but NOT transparent (so crosshair is visible) + let mut ex_style = winapi::um::winuser::GetWindowLongPtrW( + hwnd_raw as winapi::shared::windef::HWND, + winapi::um::winuser::GWL_EXSTYLE as i32 + ); + ex_style |= (winapi::um::winuser::WS_EX_LAYERED as isize); + // Remove WS_EX_TRANSPARENT to make crosshair visible + ex_style &= !(winapi::um::winuser::WS_EX_TRANSPARENT as isize); + ex_style &= !(winapi::um::winuser::WS_EX_CLIENTEDGE as isize); + ex_style &= !(winapi::um::winuser::WS_EX_STATICEDGE as isize); + ex_style &= !(winapi::um::winuser::WS_EX_WINDOWEDGE as isize); + winapi::um::winuser::SetWindowLongPtrW( + hwnd_raw as winapi::shared::windef::HWND, + winapi::um::winuser::GWL_EXSTYLE as i32, + ex_style + ); + + // Remove standard window borders and title bar + let mut style = winapi::um::winuser::GetWindowLongPtrW( + hwnd_raw as winapi::shared::windef::HWND, + winapi::um::winuser::GWL_STYLE as i32 + ); + style &= !(winapi::um::winuser::WS_CAPTION as isize); + style &= !(winapi::um::winuser::WS_THICKFRAME as isize); + style &= !(winapi::um::winuser::WS_BORDER as isize); + style &= !(winapi::um::winuser::WS_DLGFRAME as isize); + style &= !(winapi::um::winuser::WS_SIZEBOX as isize); + style |= winapi::um::winuser::WS_POPUP as isize; + winapi::um::winuser::SetWindowLongPtrW( + hwnd_raw as winapi::shared::windef::HWND, + winapi::um::winuser::GWL_STYLE as i32, + style + ); + + // Force window update to apply changes + winapi::um::winuser::SetWindowPos( + hwnd_raw as winapi::shared::windef::HWND, + std::ptr::null_mut(), + 0, 0, 0, 0, + winapi::um::winuser::SWP_NOMOVE | winapi::um::winuser::SWP_NOSIZE | winapi::um::winuser::SWP_NOZORDER | winapi::um::winuser::SWP_FRAMECHANGED + ); + + // Set transparent color key (black) - only black becomes transparent + type SetLayeredWindowAttributesFn = unsafe extern "system" fn( + winapi::shared::windef::HWND, + winapi::shared::windef::COLORREF, + u8, + u32 + ) -> winapi::shared::minwindef::BOOL; + + let set_layered_window_attributes: SetLayeredWindowAttributesFn = + std::mem::transmute(winapi::um::libloaderapi::GetProcAddress( + user32, + b"SetLayeredWindowAttributes\0".as_ptr() as *const i8 + )); + + set_layered_window_attributes( + hwnd_raw as winapi::shared::windef::HWND, + 0x000000, // Black - only this color becomes transparent + 255, + winapi::um::winuser::LWA_COLORKEY + ); + } + } + } + }); + + log::info!("Windows overlay window created with enhanced transparency settings"); + } + + // Set initial visibility state + let is_visible = CROSSHAIR_VISIBLE.load(Ordering::SeqCst); + if !is_visible { + let _ = overlay.hide(); + } + Ok(()) } +/// Create the transparent overlay window for the crosshair display (Tauri command). +#[tauri::command] +async fn create_crosshair_window(app: AppHandle) -> Result<(), String> { + create_crosshair_window_internal(app).await +} + /// Show or hide the crosshair overlay window. #[tauri::command] async fn set_crosshair_visible(app: AppHandle, visible: bool) -> Result<(), String> { @@ -117,9 +257,18 @@ async fn toggle_crosshair(app: AppHandle) -> Result { #[tauri::command] async fn show_settings(app: AppHandle) -> Result<(), String> { if let Some(main) = app.get_webview_window("main") { + // Restore and show the window main.show().map_err(|e| e.to_string())?; - main.set_focus().map_err(|e| e.to_string())?; + // Unminimize if minimized main.unminimize().map_err(|e| e.to_string())?; + // Request user attention to flash taskbar (Windows) + main.request_user_attention(Some(tauri::UserAttentionType::Informational)) + .map_err(|e| e.to_string())?; + // Set focus + main.set_focus().map_err(|e| e.to_string())?; + log::info!("Settings window shown"); + } else { + log::warn!("Main window not found"); } Ok(()) } @@ -133,12 +282,78 @@ async fn hide_settings(app: AppHandle) -> Result<(), String> { Ok(()) } -/// Minimize to system tray. +/// Minimize to system tray - keeps window in taskbar but hidden from view #[tauri::command] async fn minimize_to_tray(app: AppHandle) -> Result<(), String> { if let Some(main) = app.get_webview_window("main") { - main.hide().map_err(|e| e.to_string())?; + // Ensure window is visible in taskbar + main.set_skip_taskbar(false).map_err(|e| e.to_string())?; + + // Minimize the window + main.minimize().map_err(|e| e.to_string())?; + + // Force window to appear in taskbar + #[cfg(target_os = "windows")] + { + if let Ok(hwnd) = main.hwnd() { + unsafe { + let hwnd_raw = hwnd.0 as isize; + let user32 = winapi::um::libloaderapi::GetModuleHandleA(b"user32\0".as_ptr() as *const i8); + if !user32.is_null() { + // Ensure window has taskbar flag + let mut ex_style = winapi::um::winuser::GetWindowLongPtrW( + hwnd_raw as winapi::shared::windef::HWND, + winapi::um::winuser::GWL_EXSTYLE as i32 + ); + ex_style &= !(winapi::um::winuser::WS_EX_TOOLWINDOW as isize); + ex_style |= winapi::um::winuser::WS_EX_APPWINDOW as isize; + winapi::um::winuser::SetWindowLongPtrW( + hwnd_raw as winapi::shared::windef::HWND, + winapi::um::winuser::GWL_EXSTYLE as i32, + ex_style + ); + + // Force window update + winapi::um::winuser::SetWindowPos( + hwnd_raw as winapi::shared::windef::HWND, + std::ptr::null_mut(), + 0, 0, 0, 0, + winapi::um::winuser::SWP_NOMOVE | winapi::um::winuser::SWP_NOSIZE | winapi::um::winuser::SWP_NOZORDER | winapi::um::winuser::SWP_FRAMECHANGED + ); + } + } + } + } + + log::info!("Window minimized to tray (visible in taskbar)"); + } + Ok(()) +} + +/// Restore window from minimized state +#[tauri::command] +async fn restore_from_tray(app: AppHandle) -> Result<(), String> { + if let Some(main) = app.get_webview_window("main") { + // Show and unminimize the window + main.unminimize().map_err(|e| e.to_string())?; + main.show().map_err(|e| e.to_string())?; + main.set_focus().map_err(|e| e.to_string())?; + log::info!("Window restored from tray"); + } + Ok(()) +} + +/// Exit the application completely. +#[tauri::command] +async fn exit_app(app: AppHandle) -> Result<(), String> { + log::info!("Exiting application..."); + if let Some(overlay) = app.get_webview_window("crosshair-layer") { + let _ = overlay.close(); + } + if let Some(main) = app.get_webview_window("main") { + let _ = main.close(); } + app.exit(0); Ok(()) } @@ -202,6 +417,16 @@ pub fn run() { .level(log::LevelFilter::Info) .build(), ) + // Single instance plugin - prevents multiple instances + .plugin(tauri_plugin_single_instance::init(|app, _argv, _cwd| { + // When a second instance is launched, focus the main window + log::info!("Another instance was launched, focusing main window"); + if let Some(main) = app.get_webview_window("main") { + let _ = main.show(); + let _ = main.unminimize(); + let _ = main.set_focus(); + } + })) .invoke_handler(tauri::generate_handler![ create_crosshair_window, set_crosshair_visible, @@ -210,20 +435,32 @@ pub fn run() { show_settings, hide_settings, minimize_to_tray, + restore_from_tray, + exit_app, register_shortcut, get_primary_monitor, emit_preset_update, ]) .setup(|app| { - // Setup system tray using JS menu API (programmatic tray setup) - // Tray setup is handled in the frontend via @tauri-apps/api/tray + let app_handle = app.handle().clone(); + let main_window = app.get_webview_window("main").unwrap(); + + main_window.on_window_event(move |event| { + if let tauri::WindowEvent::CloseRequested { .. } = event { + log::info!("Main window close requested - exiting application"); + if let Some(overlay) = app_handle.get_webview_window("crosshair-layer") { + let _ = overlay.close(); + } + app_handle.exit(0); + } + }); + log::info!("CrosshairOverlay setup started"); - // Create the crosshair overlay window after a short delay - let app_handle = app.handle().clone(); + let app_handle_clone = app.handle().clone(); tauri::async_runtime::spawn(async move { tokio::time::sleep(std::time::Duration::from_millis(800)).await; - if let Err(e) = create_crosshair_window(app_handle).await { + if let Err(e) = create_crosshair_window_internal(app_handle_clone).await { log::error!("Failed to create crosshair window: {}", e); } }); diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 32df1a1..b41de10 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "../node_modules/@tauri-apps/cli/config.schema.json", "productName": "CrosshairOverlay", - "version": "0.1.0", + "version": "1.0.0", "identifier": "com.crosshair.overlay", "build": { "frontendDist": "../dist", @@ -16,10 +16,10 @@ { "label": "main", "title": "CrosshairOverlay", - "width": 400, - "height": 600, - "minWidth": 360, - "minHeight": 400, + "width": 1024, + "height": 768, + "minWidth": 480, + "minHeight": 400, "resizable": true, "center": true, "visible": true, @@ -32,11 +32,6 @@ ], "security": { "csp": "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:;" - }, - "trayIcon": { - "iconPath": "icons/icon.png", - "iconAsTemplate": true, - "id": "main-tray" } }, "bundle": { diff --git a/src/App.tsx b/src/App.tsx index 8035efc..b660e03 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -147,7 +147,7 @@ function SettingsWindow() {
CrosshairOverlay
-
v0.1.0
+
v1.0.0
diff --git a/src/CrosshairOverlayApp.tsx b/src/CrosshairOverlayApp.tsx index a89532d..b252588 100644 --- a/src/CrosshairOverlayApp.tsx +++ b/src/CrosshairOverlayApp.tsx @@ -11,17 +11,34 @@ import { load } from '@tauri-apps/plugin-store'; const STORE_FILE = 'settings.json'; -// Detect which window we are in via URL param (?overlay=true) -const IS_OVERLAY = new URLSearchParams(window.location.search).has('overlay'); - export function CrosshairOverlayApp() { - const [config, setConfig] = useState(BUILTIN_PRESETS[0]); + const [config, setConfig] = useState(null); const [visible, setVisible] = useState(true); + const [isReady, setIsReady] = useState(false); useEffect(() => { - if (!IS_OVERLAY) return; + // Force transparent background on all elements + const setTransparent = () => { + document.documentElement.style.background = 'transparent'; + document.documentElement.style.backgroundColor = 'transparent'; + document.body.style.background = 'transparent'; + document.body.style.backgroundColor = 'transparent'; + document.body.style.margin = '0'; + document.body.style.padding = '0'; + const root = document.getElementById('root'); + if (root) { + root.style.background = 'transparent'; + root.style.backgroundColor = 'transparent'; + } + }; + + setTransparent(); - (async () => { + // Configure window for transparency via Tauri API + import('@tauri-apps/api/window').then(async ({ getCurrentWindow }) => { + const win = getCurrentWindow(); + + // Load saved settings try { const store = await load(STORE_FILE, { autoSave: false, defaults: {} }); const savedId = await store.get('currentPresetId'); @@ -35,23 +52,60 @@ export function CrosshairOverlayApp() { const custom = await store.get('customPresets'); const all = [...BUILTIN_PRESETS, ...(custom ?? [])]; const found = all.find(p => p.id === savedId); - if (found) setConfig(found); + if (found) { + setConfig(found); + } else { + setConfig(BUILTIN_PRESETS[0]); + } + } else { + setConfig(BUILTIN_PRESETS[0]); + } + setIsReady(true); + + // Show window once content is ready, but only if visible + if (savedSettings?.showCrosshair !== false) { + setTimeout(() => { + win.show().catch(() => {}); + }, 150); } } catch (e) { - // Use defaults + // Use defaults on error + setConfig(BUILTIN_PRESETS[0]); + setIsReady(true); + + setTimeout(() => { + win.show().catch(() => {}); + }, 150); } - })(); + }); // Listen for preset change events import('@tauri-apps/api/event').then(({ listen }) => { - listen<{ config: CrosshairConfig }>('overlay-update', (event) => { - setConfig(event.payload.config); + listen('overlay-update', (event) => { + setConfig(event.payload); }); - }); + + listen('hotkey-toggle', () => { + import('@tauri-apps/api/core').then(({ invoke }) => { + invoke('toggle_crosshair').catch(() => {}); + }); + }); + }).catch(() => {}); }, []); - if (!IS_OVERLAY) return null; - if (!visible) return null; + // Always render transparent container to avoid black screen + if (!visible || !config || !isReady) { + return ( +
+ ); + } return (
void; decimal?: boolean; - }) => ( -
-
- {label} - { + const [inputValue, setInputValue] = useState(String(decimal ? value.toFixed(1) : value)); + const sliderRef = useRef(null); + + // Sync input when value changes externally + useEffect(() => { + setInputValue(decimal ? value.toFixed(1) : String(value)); + }, [value, decimal]); + + const handleInputChange = (newValue: number) => { + const clamped = Math.max(min, Math.min(max, newValue)); + onChange(decimal ? Math.round(clamped * 10) / 10 : Math.round(clamped)); + }; + + const handleTextChange = (text: string) => { + setInputValue(text); + const num = parseFloat(text); + if (!isNaN(num)) { + handleInputChange(num); + } + }; + + const handleBlur = () => { + const num = parseFloat(inputValue); + if (isNaN(num)) { + setInputValue(decimal ? value.toFixed(1) : String(value)); + } else { + handleInputChange(num); + setInputValue(decimal ? value.toFixed(1) : String(value)); + } + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter') { + handleBlur(); + (e.target as HTMLInputElement).blur(); + } else if (e.key === 'ArrowUp') { + e.preventDefault(); + handleInputChange(value + step); + } else if (e.key === 'ArrowDown') { + e.preventDefault(); + handleInputChange(value - step); + } + }; + + const increment = () => handleInputChange(value + step); + const decrement = () => handleInputChange(value - step); + + return ( +
+
+ {label} +
+ + handleTextChange(e.target.value)} + onBlur={handleBlur} + onKeyDown={handleKeyDown} + style={{ + width: 52, + textAlign: 'center', + background: 'var(--bg-surface)', + border: '1px solid var(--border-default)', + borderRadius: 6, + padding: '4px 4px', + color: 'var(--text-primary)', + fontSize: 12, + fontFamily: 'ui-monospace, monospace', + fontWeight: 600, + }} + /> + {unit} + +
+
+
- {decimal ? value.toFixed(1) : value}{unit} - +
+ onChange(decimal ? parseFloat(e.target.value) : Number(e.target.value))} + onWheel={e => { + e.preventDefault(); + const delta = e.deltaY > 0 ? -step : step; + handleInputChange(value + delta); + }} + style={{ + position: 'absolute', + inset: 0, + width: '100%', + height: '100%', + opacity: 0, + cursor: 'pointer', + margin: 0, + }} + /> +
- onChange(Number(e.target.value))} - style={{ width: '100%' }} - /> -
- ); + ); + }; // Section card wrapper - const Section = ({ title, icon, children }: { title: string; icon: React.ReactNode; children: React.ReactNode }) => ( + const Section = ({ title, icon, children, gridArea }: { + title: string; + icon: React.ReactNode; + children: React.ReactNode; + gridArea?: string; + }) => (
- {icon} + {icon} {title}
-
+
{children}
@@ -96,47 +234,60 @@ export function SettingsPanel({ config, onChange, onSaveAsPreset, t }: SettingsP return (
{/* ── Preview ─────────────────────────────────────── */}
- {/* Checkerboard bg */}
+ {/* Gradient bg */}
- +
- {/* Crosshair name + style badge */} -
- +
+ {config.name || t.panel.custom} {STYLES.find(s => s.value === config.style)?.label ?? config.style} @@ -146,11 +297,11 @@ export function SettingsPanel({ config, onChange, onSaveAsPreset, t }: SettingsP {/* ── Style ───────────────────────────────────────── */}
} + icon={} >
{STYLES.map(style => { @@ -161,28 +312,30 @@ export function SettingsPanel({ config, onChange, onSaveAsPreset, t }: SettingsP onClick={() => onChange({ style: style.value })} title={style.label} style={{ - display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 4, - padding: '9px 6px', - background: isActive ? 'var(--accent-muted)' : 'transparent', - border: `1px solid ${isActive ? 'var(--accent)' : 'var(--border-default)'}`, - borderRadius: 'var(--radius-sm)', + display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 6, + padding: '12px 8px', + background: isActive ? 'var(--accent-muted)' : 'var(--bg-surface)', + border: `2px solid ${isActive ? 'var(--accent)' : 'var(--border-default)'}`, + borderRadius: 10, color: isActive ? 'var(--accent)' : 'var(--text-muted)', cursor: 'pointer', - transition: 'all 120ms', - fontSize: 9, fontWeight: isActive ? 700 : 500, + transition: 'all 150ms cubic-bezier(0.34, 1.56, 0.64, 1)', + fontSize: 10, fontWeight: isActive ? 700 : 500, }} onMouseEnter={e => { if (!isActive) { - e.currentTarget.style.borderColor = 'var(--border-strong)'; + e.currentTarget.style.borderColor = 'var(--accent)'; e.currentTarget.style.color = 'var(--text-secondary)'; e.currentTarget.style.background = 'var(--bg-hover)'; + e.currentTarget.style.transform = 'translateY(-2px)'; } }} onMouseLeave={e => { if (!isActive) { e.currentTarget.style.borderColor = 'var(--border-default)'; e.currentTarget.style.color = 'var(--text-muted)'; - e.currentTarget.style.background = 'transparent'; + e.currentTarget.style.background = 'var(--bg-surface)'; + e.currentTarget.style.transform = 'translateY(0)'; } }} > @@ -197,10 +350,9 @@ export function SettingsPanel({ config, onChange, onSaveAsPreset, t }: SettingsP {/* ── Color ───────────────────────────────────────── */}
} + icon={} > - {/* Swatches */} -
+
{PRESET_COLORS.map(c => { const isActive = config.color.toLowerCase() === c.toLowerCase(); return ( @@ -209,13 +361,13 @@ export function SettingsPanel({ config, onChange, onSaveAsPreset, t }: SettingsP onClick={() => onChange({ color: c })} title={c} style={{ - width: 22, height: 22, borderRadius: '50%', + width: 26, height: 26, borderRadius: '50%', background: c, border: `2px solid ${isActive ? 'white' : 'rgba(255,255,255,0.1)'}`, cursor: 'pointer', - transform: isActive ? 'scale(1.2)' : 'scale(1)', - transition: 'transform 150ms cubic-bezier(0.34, 1.56, 0.64, 1), border-color 120ms', - boxShadow: isActive ? `0 0 0 2px var(--accent-glow)` : 'none', + transform: isActive ? 'scale(1.25)' : 'scale(1)', + transition: 'transform 200ms cubic-bezier(0.34, 1.56, 0.64, 1), border-color 150ms', + boxShadow: isActive ? `0 0 0 3px var(--accent), 0 2px 8px rgba(0,0,0,0.3)` : '0 2px 4px rgba(0,0,0,0.2)', }} onMouseEnter={e => { if (!isActive) e.currentTarget.style.transform = 'scale(1.15)'; }} onMouseLeave={e => { if (!isActive) e.currentTarget.style.transform = 'scale(1)'; }} @@ -223,23 +375,29 @@ export function SettingsPanel({ config, onChange, onSaveAsPreset, t }: SettingsP ); })}
- {/* Hex input */} -
- onChange({ color: e.target.value })} - style={{ width: 32, height: 28, borderRadius: 'var(--radius-sm)', cursor: 'pointer', flexShrink: 0 }} - /> +
+
+ onChange({ color: e.target.value })} + style={{ width: '100%', height: '100%', border: 'none', padding: 0, cursor: 'pointer' }} + /> +
- # + # - {/* Live color preview */}
@@ -271,7 +429,7 @@ export function SettingsPanel({ config, onChange, onSaveAsPreset, t }: SettingsP {/* ── Dimensions ──────────────────────────────────── */}
} + icon={} > onChange({ size: v })} /> @@ -286,7 +444,7 @@ export function SettingsPanel({ config, onChange, onSaveAsPreset, t }: SettingsP {/* ── Visual ──────────────────────────────────────── */}
} + icon={} > onChange({ opacity: v })} /> @@ -296,24 +454,49 @@ export function SettingsPanel({ config, onChange, onSaveAsPreset, t }: SettingsP {/* Outline toggle */}
-
- {t.panel.outline} - {t.panel.outlineDesc} +
+ {t.panel.outline} + {t.panel.outlineDesc}
-
+
{config.outline && ( - onChange({ outlineColor: e.target.value })} - style={{ width: 24, height: 24, borderRadius: 4, cursor: 'pointer' }} - /> +
+ onChange({ outlineColor: e.target.value })} + style={{ width: '100%', height: '100%', border: 'none', padding: 0, cursor: 'pointer' }} + /> +
)} - onChange({ outline: e.target.checked })} /> +
@@ -321,21 +504,41 @@ export function SettingsPanel({ config, onChange, onSaveAsPreset, t }: SettingsP {/* ── Animation ───────────────────────────────────── */}
} + icon={} >
-
- {t.panel.pulseEffect} - {t.panel.pulseDesc} +
+ {t.panel.pulseEffect} + {t.panel.pulseDesc}
- onChange({ animated: e.target.checked })} /> +
{config.animated && ( - setPresetName(e.target.value)} - placeholder={t.panel.customName} - maxLength={32} - style={{ - width: '100%', +
+
{ e.currentTarget.style.borderColor = 'var(--accent)'; }} - onBlur={e => { e.currentTarget.style.borderColor = 'var(--border-default)'; }} - /> + }}> + + + + setPresetName(e.target.value)} + placeholder={t.panel.customName} + maxLength={32} + style={{ + flex: 1, + background: 'transparent', + border: 'none', + outline: 'none', + color: 'var(--text-primary)', + fontSize: 13, + padding: '12px 0', + }} + /> + {presetName && ( + + {presetName.length}/32 + + )} +
+