134 lines
4.3 KiB
TypeScript
134 lines
4.3 KiB
TypeScript
import { StrictMode } from 'react'
|
|
import * as Sentry from '@sentry/react'
|
|
import { createRoot } from 'react-dom/client'
|
|
import { TooltipProvider } from '@/components/ui/tooltip'
|
|
import './index.css'
|
|
import App from './App.tsx'
|
|
import { FrontendReadyMarker } from './components/FrontendReadyMarker'
|
|
import { LinuxTitlebar } from './components/LinuxTitlebar'
|
|
import { applyStoredThemeMode } from './lib/themeMode'
|
|
import {
|
|
APP_COMMAND_EVENT_NAME,
|
|
isAppCommandId,
|
|
isNativeMenuCommandId,
|
|
} from './hooks/appCommandDispatcher'
|
|
import {
|
|
getShortcutEventInit,
|
|
type AppCommandShortcutEventInit,
|
|
type AppCommandShortcutEventOptions,
|
|
} from './hooks/appCommandCatalog'
|
|
import { shouldUseLinuxWindowChrome } from './utils/platform'
|
|
import { reloadFrontendOnceIfStartupFailed } from './utils/frontendReady'
|
|
|
|
const EDITOR_DROP_SELECTOR = '.editor__blocknote-container'
|
|
|
|
function dataTransferHasFiles(dataTransfer: DataTransfer | null): boolean {
|
|
if (!dataTransfer) return false
|
|
if (dataTransfer.files.length > 0) return true
|
|
if (Array.from(dataTransfer.types).includes('Files')) return true
|
|
|
|
return Array.from(dataTransfer.items).some((item) => item.kind === 'file')
|
|
}
|
|
|
|
function isEditorDropTarget(target: EventTarget | null): boolean {
|
|
return target instanceof Element && target.closest(EDITOR_DROP_SELECTOR) !== null
|
|
}
|
|
|
|
function preventFileDropNavigation(event: DragEvent): void {
|
|
if (isEditorDropTarget(event.target)) return
|
|
if (!dataTransferHasFiles(event.dataTransfer)) return
|
|
|
|
event.preventDefault()
|
|
}
|
|
|
|
document.addEventListener('dragover', preventFileDropNavigation, true)
|
|
document.addEventListener('drop', preventFileDropNavigation, true)
|
|
|
|
// Disable native WebKit context menu in Tauri (WKWebView intercepts right-click
|
|
// at native level before React's synthetic events can call preventDefault).
|
|
// Capture phase fires first → prevents native menu; React bubble phase still fires
|
|
// → our custom context menus (e.g. sidebar right-click) work correctly.
|
|
if ('__TAURI__' in window || '__TAURI_INTERNALS__' in window) {
|
|
document.addEventListener('contextmenu', (e) => e.preventDefault(), true)
|
|
}
|
|
|
|
if (shouldUseLinuxWindowChrome()) {
|
|
document.body.classList.add('linux-chrome')
|
|
}
|
|
|
|
applyStoredThemeMode(document, window.localStorage)
|
|
|
|
function dispatchDeterministicShortcutEvent(init: AppCommandShortcutEventInit) {
|
|
const target =
|
|
document.activeElement instanceof HTMLElement
|
|
? document.activeElement
|
|
: document.body ?? window
|
|
|
|
target.dispatchEvent(new KeyboardEvent('keydown', init))
|
|
}
|
|
|
|
window.__laputaTest = {
|
|
dispatchAppCommand(id: string) {
|
|
if (!isAppCommandId(id)) {
|
|
throw new Error(`Unknown app command: ${id}`)
|
|
}
|
|
window.dispatchEvent(new CustomEvent(APP_COMMAND_EVENT_NAME, { detail: id }))
|
|
},
|
|
dispatchShortcutEvent(init: AppCommandShortcutEventInit) {
|
|
dispatchDeterministicShortcutEvent(init)
|
|
},
|
|
async triggerMenuCommand(id: string) {
|
|
if (!isNativeMenuCommandId(id)) {
|
|
throw new Error(`Unknown native menu command: ${id}`)
|
|
}
|
|
|
|
if ('__TAURI__' in window || '__TAURI_INTERNALS__' in window) {
|
|
const { invoke } = await import('@tauri-apps/api/core')
|
|
return invoke('trigger_menu_command', { id })
|
|
}
|
|
|
|
if (!window.__laputaTest?.dispatchBrowserMenuCommand) {
|
|
throw new Error('Tolaria test bridge is missing dispatchBrowserMenuCommand')
|
|
}
|
|
|
|
window.__laputaTest.dispatchBrowserMenuCommand(id)
|
|
return undefined
|
|
},
|
|
triggerShortcutCommand(id: string, options?: AppCommandShortcutEventOptions) {
|
|
if (!isAppCommandId(id)) {
|
|
throw new Error(`Unknown app command: ${id}`)
|
|
}
|
|
|
|
const init = getShortcutEventInit(id, options)
|
|
if (!init) {
|
|
throw new Error(`Command ${id} does not define a keyboard shortcut`)
|
|
}
|
|
|
|
dispatchDeterministicShortcutEvent(init)
|
|
},
|
|
}
|
|
|
|
const sentryReactErrorHandler = Sentry.reactErrorHandler()
|
|
|
|
function captureReactRootError(
|
|
error: unknown,
|
|
errorInfo: { componentStack?: string },
|
|
): void {
|
|
sentryReactErrorHandler(error, { componentStack: errorInfo.componentStack ?? '' })
|
|
reloadFrontendOnceIfStartupFailed()
|
|
}
|
|
|
|
createRoot(document.getElementById('root')!, {
|
|
onCaughtError: captureReactRootError,
|
|
onUncaughtError: captureReactRootError,
|
|
onRecoverableError: captureReactRootError,
|
|
}).render(
|
|
<StrictMode>
|
|
<TooltipProvider>
|
|
<LinuxTitlebar />
|
|
<App />
|
|
<FrontendReadyMarker />
|
|
</TooltipProvider>
|
|
</StrictMode>,
|
|
)
|