diff --git a/src-tauri/src/config.rs b/src-tauri/src/config.rs index 9a418dc..e1c04a3 100644 --- a/src-tauri/src/config.rs +++ b/src-tauri/src/config.rs @@ -22,6 +22,24 @@ pub enum Theme { Dark, } +/// Native webview background color matching the frontend's `--bg-main`. +/// Windows-only: WebView2 paints its default white before content renders, +/// which flashes on window-open and shows as a blank strip at the growing +/// edges during live resize. macOS ignores `background_color` (Tauri no-op) +/// and Linux/WebKitGTK never had the flash, so this is gated to Windows at the +/// call sites. +/// +/// System resolves to light: dark mode is an opt-in `[data-theme="dark"]` +/// override, so light is today's effective default. Light `--bg-main` is +/// `#fafaf9`; dark is `#1d1b16`. +#[cfg(windows)] +pub fn window_background_color(theme: Theme) -> tauri::webview::Color { + match theme { + Theme::Dark => tauri::webview::Color(0x1d, 0x1b, 0x16, 0xff), + Theme::Light | Theme::System => tauri::webview::Color(0xfa, 0xfa, 0xf9, 0xff), + } +} + /// Application configuration stored in config.json. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Config { @@ -607,6 +625,26 @@ mod tests { assert_eq!(config.theme, Theme::System); } + #[cfg(windows)] + #[test] + fn window_background_matches_css_bg_main() { + // Must match `--bg-main` in src/styles/tokens.css, or WebView2 flashes + // its default background on open/resize. System resolves to light + // because dark is an opt-in [data-theme="dark"] override. + assert_eq!( + window_background_color(Theme::Light), + tauri::webview::Color(0xfa, 0xfa, 0xf9, 0xff) + ); + assert_eq!( + window_background_color(Theme::System), + window_background_color(Theme::Light) + ); + assert_eq!( + window_background_color(Theme::Dark), + tauri::webview::Color(0x1d, 0x1b, 0x16, 0xff) + ); + } + #[test] fn config_deserializes_without_theme() { // Old configs without theme field should default to System diff --git a/src-tauri/src/excalidraw_window.rs b/src-tauri/src/excalidraw_window.rs index 38d148a..a9ea815 100644 --- a/src-tauri/src/excalidraw_window.rs +++ b/src-tauri/src/excalidraw_window.rs @@ -156,6 +156,11 @@ pub async fn open_excalidraw_window( .inner_size(width, height) .min_inner_size(600.0, 400.0) .visible(false); + // Windows-only: solid themed bg so WebView2 doesn't flash white. + #[cfg(windows)] + let b = b.background_color(crate::config::window_background_color( + crate::config::load_config().theme, + )); #[cfg(target_os = "macos")] let b = b .title_bar_style(tauri::TitleBarStyle::Overlay) diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 81768fe..39f3ecd 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -131,6 +131,11 @@ pub fn run(state: AppState, context: tauri::Context, json_output: bool) { .title("annot") .inner_size(1000.0, 700.0) .visible(false); // Will be shown after content loads + // Windows-only: paint a solid themed bg so WebView2 doesn't + // flash its default white on open/resize (see config helper). + #[cfg(windows)] + let b = b + .background_color(config::window_background_color(config::load_config().theme)); #[cfg(target_os = "macos")] let b = b .title_bar_style(tauri::TitleBarStyle::Overlay) diff --git a/src-tauri/src/mcp/mod.rs b/src-tauri/src/mcp/mod.rs index ba84029..746fde5 100644 --- a/src-tauri/src/mcp/mod.rs +++ b/src-tauri/src/mcp/mod.rs @@ -314,6 +314,11 @@ fn run_session_with_state( .title("annot") .inner_size(1000.0, 700.0) .visible(false); // Will be shown after content loads + // Windows-only: solid themed bg so WebView2 doesn't flash white. + #[cfg(windows)] + let b = b.background_color(crate::config::window_background_color( + crate::config::load_config().theme, + )); #[cfg(target_os = "macos")] let b = b .title_bar_style(tauri::TitleBarStyle::Overlay) diff --git a/src-tauri/src/mermaid_window.rs b/src-tauri/src/mermaid_window.rs index 6664f39..b127352 100644 --- a/src-tauri/src/mermaid_window.rs +++ b/src-tauri/src/mermaid_window.rs @@ -105,6 +105,11 @@ pub async fn open_mermaid_window( .inner_size(600.0, 500.0) .min_inner_size(300.0, 200.0) .visible(false); + // Windows-only: solid themed bg so WebView2 doesn't flash white. + #[cfg(windows)] + let b = b.background_color(crate::config::window_background_color( + crate::config::load_config().theme, + )); #[cfg(target_os = "macos")] let b = b .title_bar_style(tauri::TitleBarStyle::Overlay) diff --git a/src/lib/AnnotationEditor.svelte b/src/lib/AnnotationEditor.svelte index 7988051..b307422 100644 --- a/src/lib/AnnotationEditor.svelte +++ b/src/lib/AnnotationEditor.svelte @@ -108,13 +108,14 @@ annotationEntries?: Record; allowsImagePaste?: boolean; onImagePasteBlocked?: () => void; + onFileRefCopied?: (path: string) => void; onRequestCreateTag?: (text: string, from: number, to: number) => void; pendingTagInsertion?: { from: number; to: number; tag: Tag } | null; rangeKey?: string; // Annotation line range key like "45-52" getOriginalLines?: () => string; // Returns original lines content for /replace } - let { content, onUpdate, sealed = false, onUnseal, onDismiss, tags = [], annotationEntries = {}, allowsImagePaste = false, onImagePasteBlocked, onRequestCreateTag, pendingTagInsertion, rangeKey = '', getOriginalLines }: Props = $props(); + let { content, onUpdate, sealed = false, onUnseal, onDismiss, tags = [], annotationEntries = {}, allowsImagePaste = false, onImagePasteBlocked, onFileRefCopied, onRequestCreateTag, pendingTagInsertion, rangeKey = '', getOriginalLines }: Props = $props(); // Get zoom level from context for floating elements const ctx = getAnnotContext(); @@ -341,8 +342,14 @@ openExcalidrawEdit(detail.nodeId, detail.elements); }; + const handleFileRefCopied = (e: Event) => { + const detail = (e as CustomEvent).detail as { path: string }; + onFileRefCopied?.(detail.path); + }; + element?.addEventListener('excalidraw-create', handleExcalidrawCreate); element?.addEventListener('excalidraw-edit', handleExcalidrawEdit); + element?.addEventListener('file-ref-copied', handleFileRefCopied); // Listen for placeholder destruction (dispatched on document) document.addEventListener('excalidraw-placeholder-destroyed', handlePlaceholderDestroyed); @@ -392,6 +399,7 @@ return () => { element?.removeEventListener('excalidraw-create', handleExcalidrawCreate); element?.removeEventListener('excalidraw-edit', handleExcalidrawEdit); + element?.removeEventListener('file-ref-copied', handleFileRefCopied); document.removeEventListener('excalidraw-placeholder-destroyed', handlePlaceholderDestroyed); }; }); diff --git a/src/lib/components/AnnotationSlot.svelte b/src/lib/components/AnnotationSlot.svelte index 958dbdf..c0e83a6 100644 --- a/src/lib/components/AnnotationSlot.svelte +++ b/src/lib/components/AnnotationSlot.svelte @@ -15,6 +15,7 @@ onDismiss: () => void; onRequestCreateTag: (rangeKey: string, text: string, from: number, to: number) => void; onImagePasteBlocked: () => void; + onFileRefCopied?: (path: string) => void; } @@ -38,6 +39,7 @@ onDismiss, onRequestCreateTag, onImagePasteBlocked, + onFileRefCopied, }: AnnotationSlotProps = $props(); const ctx = getAnnotContext(); @@ -58,6 +60,7 @@ annotationEntries={ctx.annotations.allEntries()} allowsImagePaste={ctx.allowsImagePaste} {onImagePasteBlocked} + {onFileRefCopied} onRequestCreateTag={(text, from, to) => onRequestCreateTag(rangeKey, text, from, to)} pendingTagInsertion={pendingTagInsertion?.editorKey === rangeKey ? { from: pendingTagInsertion.from, to: pendingTagInsertion.to, tag: pendingTagInsertion.tag } diff --git a/src/lib/components/SessionEditor.svelte b/src/lib/components/SessionEditor.svelte index 3454e6d..a43dee5 100644 --- a/src/lib/components/SessionEditor.svelte +++ b/src/lib/components/SessionEditor.svelte @@ -16,6 +16,7 @@ onClose: () => void; onRequestCreateTag: (text: string, from: number, to: number) => void; onImagePasteBlocked: () => void; + onFileRefCopied?: (path: string) => void; } let { @@ -26,7 +27,8 @@ onOpen, onClose, onRequestCreateTag, - onImagePasteBlocked + onImagePasteBlocked, + onFileRefCopied }: Props = $props(); const ctx = getAnnotContext(); @@ -44,6 +46,7 @@ annotationEntries={ctx.annotations.allEntries()} allowsImagePaste={ctx.allowsImagePaste} {onImagePasteBlocked} + {onFileRefCopied} {onRequestCreateTag} {pendingTagInsertion} /> diff --git a/src/lib/tiptap/nodeviews/FileRefChip.svelte b/src/lib/tiptap/nodeviews/FileRefChip.svelte index f340717..b13ad35 100644 --- a/src/lib/tiptap/nodeviews/FileRefChip.svelte +++ b/src/lib/tiptap/nodeviews/FileRefChip.svelte @@ -11,13 +11,16 @@ return parts.length > 1 ? parts[parts.length - 2] : null; }); + let chipEl: HTMLSpanElement; + async function copyPath() { await navigator.clipboard.writeText(path); - // TODO: show toast via event + chipEl?.dispatchEvent(new CustomEvent('file-ref-copied', { bubbles: true, detail: { path } })); }