diff --git a/package-lock.json b/package-lock.json index 83f6291..8093e59 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "promptlight-scaffold", - "version": "1.0.0", + "version": "1.1.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "promptlight-scaffold", - "version": "1.0.0", + "version": "1.1.2", "dependencies": { "@tauri-apps/api": "^2", "@tauri-apps/plugin-clipboard-manager": "^2.3.2", diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index 8eea40a..603c269 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -11,6 +11,7 @@ "global-shortcut:allow-unregister", "global-shortcut:allow-is-registered", "clipboard-manager:allow-write-text", - "clipboard-manager:allow-read-text" + "clipboard-manager:allow-read-text", + "core:window:allow-set-always-on-top" ] } diff --git a/src-tauri/src/data/settings.rs b/src-tauri/src/data/settings.rs index 7da5c02..dcbb08a 100644 --- a/src-tauri/src/data/settings.rs +++ b/src-tauri/src/data/settings.rs @@ -12,6 +12,9 @@ pub struct GeneralSettings { /// None means no hotkey is registered #[serde(default = "default_hotkey")] pub hotkey: Option, + /// Whether the editor window floats above other windows + #[serde(default = "default_editor_always_on_top")] + pub editor_always_on_top: bool, } /// Default hotkey: Cmd/Ctrl+Shift+Space @@ -19,11 +22,17 @@ fn default_hotkey() -> Option { Some("CommandOrControl+Shift+Space".to_string()) } +/// Default editor always on top: true +fn default_editor_always_on_top() -> bool { + true +} + impl Default for GeneralSettings { fn default() -> Self { Self { auto_launch: false, hotkey: default_hotkey(), + editor_always_on_top: default_editor_always_on_top(), } } } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index f2c990f..bf48b1d 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -125,6 +125,7 @@ pub fn run() { os::paste::paste_from_editor, os::paste::copy_as_markdown_file, os::window::open_editor_window, + os::window::close_editor_window, // Hotkey commands os::hotkey::get_current_hotkey, os::hotkey::set_hotkey, diff --git a/src-tauri/src/os/hotkey.rs b/src-tauri/src/os/hotkey.rs index 6df2d5f..d36baa8 100644 --- a/src-tauri/src/os/hotkey.rs +++ b/src-tauri/src/os/hotkey.rs @@ -10,6 +10,16 @@ use crate::data::settings::AppSettings; use crate::os::focus::get_key_window_screen_bounds; use crate::os::previous_app; +/// Close the editor window if it exists and always-on-top mode is enabled +fn close_editor_if_always_on_top(app: &AppHandle) { + let settings = AppSettings::load(); + if settings.general.editor_always_on_top { + if let Some(editor) = app.get_webview_window("editor") { + let _ = editor.close(); + } + } +} + const WINDOW_WIDTH: f64 = 650.0; /// State to track the currently registered shortcut @@ -173,6 +183,9 @@ pub fn register_hotkey(app: &AppHandle, hotkey_str: &str) -> Result<(), String> if window.is_visible().unwrap_or(false) { let _ = window.hide(); } else { + // Close editor window if in always-on-top mode (they shouldn't coexist) + close_editor_if_always_on_top(&app_handle); + // Capture previous app before showing (for paste-back feature) if let Err(e) = previous_app::capture_previous_app() { eprintln!("[hotkey] Failed to capture previous app: {}", e); diff --git a/src-tauri/src/os/window.rs b/src-tauri/src/os/window.rs index c7a1d9a..243c221 100644 --- a/src-tauri/src/os/window.rs +++ b/src-tauri/src/os/window.rs @@ -1,6 +1,8 @@ use serde::Deserialize; use tauri::{AppHandle, Emitter, Manager, WebviewUrl, WebviewWindowBuilder}; +use crate::data::settings::AppSettings; + /// Screen bounds passed from frontend for window positioning #[derive(Debug, Clone, Deserialize)] pub struct ScreenBounds { @@ -13,18 +15,27 @@ pub struct ScreenBounds { /// Open the editor window, optionally loading a specific prompt /// If screen_bounds is provided, the window will be centered on that screen /// view parameter can be "prompts" (default) or "settings" +/// always_on_top parameter overrides the setting (used when opening from launcher) #[tauri::command] pub async fn open_editor_window( app: AppHandle, prompt_id: Option, screen_bounds: Option, view: Option, + always_on_top: Option, ) -> Result<(), String> { let label = "editor"; let view_mode = view.as_deref().unwrap_or("prompts"); + // Get always_on_top from parameter or settings + let settings = AppSettings::load(); + let is_always_on_top = always_on_top.unwrap_or(settings.general.editor_always_on_top); + // If window already exists, show it and optionally emit event to load prompt or switch view if let Some(window) = app.get_webview_window(label) { + // Update always-on-top state in case setting changed + window.set_always_on_top(is_always_on_top).map_err(|e| e.to_string())?; + window.show().map_err(|e| e.to_string())?; window.set_focus().map_err(|e| e.to_string())?; @@ -58,7 +69,16 @@ pub async fn open_editor_window( let mut builder = WebviewWindowBuilder::new(&app, label, WebviewUrl::App(url.into())) .title("PromptLight Editor") .inner_size(window_width, window_height) - .min_inner_size(800.0, 600.0); + .min_inner_size(800.0, 600.0) + .always_on_top(is_always_on_top); + + // In always-on-top mode, hide minimize/maximize buttons (macOS) + #[cfg(target_os = "macos")] + if is_always_on_top { + builder = builder + .minimizable(false) + .maximizable(false); + } // Position on provided screen or center as fallback if let Some(bounds) = screen_bounds { @@ -73,3 +93,12 @@ pub async fn open_editor_window( Ok(()) } + +/// Close the editor window if it exists +#[tauri::command] +pub fn close_editor_window(app: AppHandle) -> Result<(), String> { + if let Some(window) = app.get_webview_window("editor") { + window.close().map_err(|e| e.to_string())?; + } + Ok(()) +} diff --git a/src/components/editor/PromptEditor/MarkdownEditor/MarkdownEditor.tsx b/src/components/editor/PromptEditor/MarkdownEditor/MarkdownEditor.tsx index c4af40c..a46b360 100644 --- a/src/components/editor/PromptEditor/MarkdownEditor/MarkdownEditor.tsx +++ b/src/components/editor/PromptEditor/MarkdownEditor/MarkdownEditor.tsx @@ -42,8 +42,19 @@ export function MarkdownEditor({ // Clear any existing content (handles React StrictMode double-mount) containerRef.current.innerHTML = ''; - // Create keyboard shortcuts for bold (Cmd+B) and italic (Cmd+I) + // Create keyboard shortcuts for formatting and selection const formattingKeymap = keymap.of([ + { + key: 'Mod-a', + run: () => { + // Use ink-mde's native API to select all text + if (editorRef.current) { + const doc = editorRef.current.getDoc(); + editorRef.current.select({ selections: [{ start: 0, end: doc.length }] }); + } + return true; + }, + }, { key: 'Mod-b', run: () => { diff --git a/src/components/editor/Sidebar/Sidebar.module.css b/src/components/editor/Sidebar/Sidebar.module.css index 1b4a251..a6aa080 100644 --- a/src/components/editor/Sidebar/Sidebar.module.css +++ b/src/components/editor/Sidebar/Sidebar.module.css @@ -12,8 +12,9 @@ min-width: 40px; background: var(--bg-secondary); display: flex; - align-items: flex-start; - justify-content: center; + flex-direction: column; + align-items: center; + gap: var(--space-xs); padding-top: var(--space-md); } @@ -33,6 +34,21 @@ color: var(--text-primary); } +.collapsedNewButton { + background: none; + border: none; + color: var(--text-secondary); + cursor: pointer; + padding: var(--space-sm); + border-radius: var(--radius-sm); + transition: background var(--transition-fast), color var(--transition-fast); +} + +.collapsedNewButton:hover { + background: var(--selection-bg); + color: var(--accent-primary); +} + .header { display: flex; align-items: center; diff --git a/src/components/editor/Sidebar/Sidebar.tsx b/src/components/editor/Sidebar/Sidebar.tsx index 3892c7b..7a88b88 100644 --- a/src/components/editor/Sidebar/Sidebar.tsx +++ b/src/components/editor/Sidebar/Sidebar.tsx @@ -119,6 +119,13 @@ export function Sidebar() { > + ); } diff --git a/src/components/editor/settings/SettingsView.tsx b/src/components/editor/settings/SettingsView.tsx index a3423ca..776359e 100644 --- a/src/components/editor/settings/SettingsView.tsx +++ b/src/components/editor/settings/SettingsView.tsx @@ -1,5 +1,5 @@ import { useEffect, useState } from 'react'; -import { Settings, Cloud, CloudOff, Power, LogOut, User, Keyboard, Palette, Sun, Moon, Monitor } from 'lucide-react'; +import { Settings, Cloud, CloudOff, Power, LogOut, User, Keyboard, Palette, Sun, Moon, Monitor, Layers } from 'lucide-react'; import { getVersion } from '@tauri-apps/api/app'; import { useSettingsStore, type AppearanceSettings } from '../../../stores/settingsStore'; import { useAuthStore } from '../../../stores/authStore'; @@ -22,6 +22,7 @@ export function SettingsView() { updateSyncSettings, setAutoLaunch, setHotkey, + setEditorAlwaysOnTop, setTheme, setAccentColor, } = useSettingsStore(); @@ -88,6 +89,8 @@ export function SettingsView() { onAutoLaunchChange={setAutoLaunch} hotkey={settings.general.hotkey} onHotkeyChange={setHotkey} + editorAlwaysOnTop={settings.general.editorAlwaysOnTop} + onEditorAlwaysOnTopChange={setEditorAlwaysOnTop} isSaving={isSaving} /> )} @@ -123,6 +126,8 @@ interface GeneralSectionProps { onAutoLaunchChange: (value: boolean) => void; hotkey: string | null; onHotkeyChange: (value: string | null) => void; + editorAlwaysOnTop: boolean; + onEditorAlwaysOnTopChange: (value: boolean) => void; isSaving: boolean; } @@ -131,6 +136,8 @@ function GeneralSection({ onAutoLaunchChange, hotkey, onHotkeyChange, + editorAlwaysOnTop, + onEditorAlwaysOnTopChange, isSaving, }: GeneralSectionProps) { return ( @@ -154,6 +161,27 @@ function GeneralSection({ +
+
+
+ + Floating editor +
+
+ Keep editor above other windows. Opening the launcher will close the editor. +
+
+ +
+
diff --git a/src/hooks/useEditorKeyboard.ts b/src/hooks/useEditorKeyboard.ts index df8061e..4934a54 100644 --- a/src/hooks/useEditorKeyboard.ts +++ b/src/hooks/useEditorKeyboard.ts @@ -1,5 +1,4 @@ import { useEffect } from 'react'; -import { getCurrentWindow } from '@tauri-apps/api/window'; import { useEditorStore } from '../stores/editorStore'; import { HOTKEYS } from '../config/constants'; @@ -11,21 +10,7 @@ export function useEditorKeyboard() { useEffect(() => { const handleKeyDown = async (e: KeyboardEvent) => { - // Escape key - save (if dirty) then close window - if (e.key === 'Escape') { - e.preventDefault(); - - // Auto-save is always on, but save explicitly if dirty - if (isDirty) { - await save(); - } - - // Close the editor window - await getCurrentWindow().close(); - return; - } - - // Only handle Cmd/Ctrl combinations below + // Only handle Cmd/Ctrl combinations if (!(e.metaKey || e.ctrlKey)) { return; } diff --git a/src/services/backend/authTypes.ts b/src/services/backend/authTypes.ts index 24222b8..3054372 100644 --- a/src/services/backend/authTypes.ts +++ b/src/services/backend/authTypes.ts @@ -30,6 +30,7 @@ export interface AuthSession { export interface GeneralSettings { autoLaunch: boolean; hotkey: string | null; + editorAlwaysOnTop: boolean; } /** Cloud sync settings */ diff --git a/src/services/backend/mockAdapter.ts b/src/services/backend/mockAdapter.ts index 6957b9a..bb72cac 100644 --- a/src/services/backend/mockAdapter.ts +++ b/src/services/backend/mockAdapter.ts @@ -32,6 +32,7 @@ const defaultSettings: AppSettings = { general: { autoLaunch: false, hotkey: 'CommandOrControl+Shift+Space', + editorAlwaysOnTop: true, }, sync: { enabled: false, diff --git a/src/stores/settingsStore.ts b/src/stores/settingsStore.ts index cb73da6..dd7ebd9 100644 --- a/src/stores/settingsStore.ts +++ b/src/stores/settingsStore.ts @@ -13,6 +13,8 @@ export interface GeneralSettings { autoLaunch: boolean; /** Global hotkey to summon the launcher (e.g., "CommandOrControl+Shift+Space") */ hotkey: string | null; + /** Whether the editor window should float above other windows (default: true) */ + editorAlwaysOnTop: boolean; } /** Cloud sync settings */ @@ -60,6 +62,8 @@ interface SettingsActions { setAutoLaunch: (enabled: boolean) => Promise; /** Set global hotkey (null to disable) */ setHotkey: (hotkey: string | null) => Promise; + /** Set editor always-on-top mode */ + setEditorAlwaysOnTop: (enabled: boolean) => Promise; /** Set theme */ setTheme: (theme: ThemeOption) => Promise; /** Set accent color */ @@ -74,6 +78,7 @@ const defaultSettings: AppSettings = { general: { autoLaunch: false, hotkey: 'CommandOrControl+Shift+Space', + editorAlwaysOnTop: true, }, sync: { enabled: false, @@ -119,9 +124,13 @@ export const useSettingsStore = create((set, get) => ({ backend.getSettings(), backend.getAutoStartEnabled().catch(() => false), ]); - // Ensure appearance has defaults if missing (backwards compat) + // Ensure fields have defaults if missing (backwards compat) const normalizedSettings: AppSettings = { ...settings, + general: { + ...settings.general, + editorAlwaysOnTop: settings.general?.editorAlwaysOnTop ?? true, + }, appearance: { theme: settings.appearance?.theme ?? DEFAULT_THEME, accentColor: settings.appearance?.accentColor ?? DEFAULT_ACCENT_COLOR, @@ -250,6 +259,11 @@ export const useSettingsStore = create((set, get) => ({ } }, + setEditorAlwaysOnTop: async (enabled) => { + const { updateGeneralSettings } = get(); + await updateGeneralSettings({ editorAlwaysOnTop: enabled }); + }, + clearError: () => { set({ error: null }); },