diff --git a/src/App.tsx b/src/App.tsx index 7ce4a40..a0cfbf2 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,8 +1,10 @@ import { useState, useEffect, useLayoutEffect, useCallback, useRef, useMemo } from "react"; import { open } from "@tauri-apps/plugin-dialog"; +import { openUrl } from "@tauri-apps/plugin-opener"; import { readTextFile } from "@tauri-apps/plugin-fs"; import { getCurrentWindow } from "@tauri-apps/api/window"; import { getCurrentWebview } from "@tauri-apps/api/webview"; +import { invoke } from "@tauri-apps/api/core"; import { listen } from "@tauri-apps/api/event"; import MarkdownIt from "markdown-it"; import hljs from "highlight.js"; @@ -337,6 +339,21 @@ function App() { }; }, [activeTab, uiZoom]); + // Open external links in default OS browser + useEffect(() => { + const handleLinkClick = (e: MouseEvent) => { + const anchor = (e.target as HTMLElement).closest('a[href]') as HTMLAnchorElement | null; + if (!anchor) return; + const href = anchor.getAttribute('href'); + if (href && /^https?:\/\//.test(href)) { + e.preventDefault(); + openUrl(href); + } + }; + document.addEventListener('click', handleLinkClick); + return () => document.removeEventListener('click', handleLinkClick); + }, []); + // Clear selection overlay when context menu closes useEffect(() => { if (!contextMenu) setSelectionRects(prev => prev.length > 0 ? [] : prev); @@ -514,25 +531,32 @@ function App() { }, [parseHeadings]); // Handle file opened via OS file association (e.g. double-clicking a .md file) + useEffect(() => { + invoke<{ path: string; content: string } | null>('get_cli_file').then(result => { + if (!result) return; + const title = result.path.split(/[/\\]/).pop() || 'Untitled'; + const newTab: Tab = { id: Date.now().toString(), title, content: result.content, path: result.path }; + setTabs(prev => [...prev, newTab]); + setActiveTabId(newTab.id); + parseHeadings(result.content); + }).catch(err => console.error('Failed to open file from CLI args:', err)); + }, []); + + // Handle files opened from a second instance (single-instance plugin forwards them here) useEffect(() => { let unlisten: (() => void) | undefined; - listen('open-file', async (event) => { - const filePath = event.payload; - try { - const existing = tabsRef.current.find(t => t.path === filePath); - if (existing) { - setActiveTabId(existing.id); - parseHeadings(existing.content); - } else { - const content = await readTextFile(filePath); - const title = filePath.split(/[/\\]/).pop() || 'Untitled'; - const newTab: Tab = { id: Date.now().toString(), title, content, path: filePath }; - setTabs(prev => [...prev, newTab]); - setActiveTabId(newTab.id); - parseHeadings(content); - } - } catch (err) { - console.error('Failed to open file from OS:', err); + listen<{ path: string; content: string }>('open-file', (event) => { + const { path: filePath, content } = event.payload; + const existing = tabsRef.current.find(t => t.path === filePath); + if (existing) { + setActiveTabId(existing.id); + parseHeadings(existing.content); + } else { + const title = filePath.split(/[/\\]/).pop() || 'Untitled'; + const newTab: Tab = { id: Date.now().toString(), title, content, path: filePath }; + setTabs(prev => [...prev, newTab]); + setActiveTabId(newTab.id); + parseHeadings(content); } }).then(fn => { unlisten = fn; }); return () => { unlisten?.(); }; @@ -726,6 +750,7 @@ function App() { return (
+ Skip to content {isDraggingOver && ( @@ -742,7 +767,7 @@ function App() { {!focusMode && ( - +
Vesper @@ -753,7 +778,7 @@ function App() {
-
+ )}
@@ -821,10 +846,10 @@ function App() {
{showSidebar && ( - +
-
Contents
+

Contents

{headings.length > 0 ? headings.map(h => ) :
No headings
@@ -832,14 +857,14 @@ function App() {
-
+ )}
-
+
{showSearch && activeTab && ( - + { setSearchQuery(e.target.value); }} onKeyDown={e => { if (e.key === 'Enter') { e.preventDefault(); setCurrentMatchIndex(i => (i + 1) % Math.max(searchMatches.length, 1)); setSearchTriggerCount(c => c + 1); } }} autoFocus /> {searchMatches.length > 0 && ( <> @@ -849,7 +874,7 @@ function App() { )} {searchQuery && searchMatches.length === 0 && No results} - + )} @@ -870,7 +895,7 @@ function App() {
-
+
diff --git a/src/styles.css b/src/styles.css index 95144b0..d14727e 100644 --- a/src/styles.css +++ b/src/styles.css @@ -153,6 +153,26 @@ html, body, #root { overflow: visible; } +/* Skip link — WCAG 2.4.1 */ +.skip-link { + position: fixed; + top: -100%; + left: 16px; + z-index: 99999; + padding: 8px 16px; + background-color: var(--color-accent); + color: white; + font-size: 13px; + font-weight: 600; + border-radius: var(--radius-md); + text-decoration: none; + transition: top 0.2s ease; +} + +.skip-link:focus { + top: 8px; +} + /* App container */ .app-container { display: flex;