diff --git a/src/frontend/components/_organisms/modals/save-changes-modal.tsx b/src/frontend/components/_organisms/modals/save-changes-modal.tsx index 6b5393c98..5955d2499 100644 --- a/src/frontend/components/_organisms/modals/save-changes-modal.tsx +++ b/src/frontend/components/_organisms/modals/save-changes-modal.tsx @@ -78,7 +78,7 @@ const SaveChangesModal = ({ isOpen, validationContext, onAfterAction, ...rest }: clearAndClose() return case 'close-app': - if (capabilities.hasNativeWindowControls) { + if (capabilities.isNativeApplication) { windowPort.quit() } return diff --git a/src/frontend/components/_organisms/title-bar/slots/center-slot.tsx b/src/frontend/components/_organisms/title-bar/slots/center-slot.tsx index dab0e2fee..8ccbef01a 100644 --- a/src/frontend/components/_organisms/title-bar/slots/center-slot.tsx +++ b/src/frontend/components/_organisms/title-bar/slots/center-slot.tsx @@ -10,10 +10,10 @@ const TitleBarCenterSlot = () => { return (
- {caps.hasNativeMenu ? ( + {caps.isNativeApplication ? ( <> OpenPLC Editor diff --git a/src/frontend/components/_organisms/title-bar/slots/left-slot.tsx b/src/frontend/components/_organisms/title-bar/slots/left-slot.tsx index e740640b3..28869a5d2 100644 --- a/src/frontend/components/_organisms/title-bar/slots/left-slot.tsx +++ b/src/frontend/components/_organisms/title-bar/slots/left-slot.tsx @@ -6,8 +6,9 @@ import { MenuBar } from '../../../_molecules/menu-bar' export const TitleBarLeftSlot = () => { const caps = useCapabilities() const path = useOpenPLCStore((state) => state.project.meta.path) + const OS = useOpenPLCStore((state) => state.workspace.systemConfigs.OS) - if (caps.hasNativeMenu) { + if (caps.isNativeApplication && OS !== 'win32') { return
} diff --git a/src/frontend/components/_organisms/title-bar/slots/right-slot.tsx b/src/frontend/components/_organisms/title-bar/slots/right-slot.tsx index 0036464ee..eea67eb4f 100644 --- a/src/frontend/components/_organisms/title-bar/slots/right-slot.tsx +++ b/src/frontend/components/_organisms/title-bar/slots/right-slot.tsx @@ -1,12 +1,14 @@ import { useCapabilities } from '../../../../../middleware/shared/providers' +import { useOpenPLCStore } from '../../../../store' import { WindowControls } from '../../../_molecules/window-controls' const TitleBarRightSlot = () => { const caps = useCapabilities() + const OS = useOpenPLCStore((state) => state.workspace.systemConfigs.OS) return (
- {caps.hasNativeWindowControls && } + {caps.isNativeApplication && OS === 'win32' && }
) } diff --git a/src/frontend/components/_templates/accelerator-handler.tsx b/src/frontend/components/_templates/accelerator-handler.tsx index 05993c66e..214596961 100644 --- a/src/frontend/components/_templates/accelerator-handler.tsx +++ b/src/frontend/components/_templates/accelerator-handler.tsx @@ -318,44 +318,44 @@ const AcceleratorHandler = () => { * Window lifecycle events (editor-only, gated by capabilities) */ useEffect(() => { - if (!capabilities.hasNativeWindowControls) return + if (!capabilities.isNativeApplication) return const unsub = windowPort.enableAutoCloseHandshake?.() return unsub - }, [capabilities.hasNativeWindowControls, windowPort]) + }, [capabilities.isNativeApplication, windowPort]) useEffect(() => { - if (!capabilities.hasNativeWindowControls) return + if (!capabilities.isNativeApplication) return const unsub = windowPort.onCloseRequested(() => { setCloseWindow(true) }) return unsub - }, [capabilities.hasNativeWindowControls, windowPort, setCloseWindow]) + }, [capabilities.isNativeApplication, windowPort, setCloseWindow]) useEffect(() => { - if (!capabilities.hasNativeWindowControls) return + if (!capabilities.isNativeApplication) return const unsub = windowPort.onDarwinAppQuitting?.(() => { setCloseAppDarwin(true) }) return unsub - }, [capabilities.hasNativeWindowControls, windowPort, setCloseAppDarwin]) + }, [capabilities.isNativeApplication, windowPort, setCloseAppDarwin]) useEffect(() => { - if (!capabilities.hasNativeWindowControls) return + if (!capabilities.isNativeApplication) return const unsub = windowPort.onMaximizedChanged?.(() => { toggleMaximizedWindow() }) return unsub - }, [capabilities.hasNativeWindowControls, windowPort, toggleMaximizedWindow]) + }, [capabilities.isNativeApplication, windowPort, toggleMaximizedWindow]) /** * beforeunload event (editor-only) */ useEffect(() => { - if (!capabilities.hasNativeWindowControls) return + if (!capabilities.isNativeApplication) return const handler = (e: BeforeUnloadEvent) => { if (capabilities.isDevMode) return @@ -380,7 +380,7 @@ const AcceleratorHandler = () => { window.addEventListener('beforeunload', handler) return () => window.removeEventListener('beforeunload', handler) }, [ - capabilities.hasNativeWindowControls, + capabilities.isNativeApplication, close.window, close.app, close.appDarwin, diff --git a/src/frontend/components/_templates/app-layout.tsx b/src/frontend/components/_templates/app-layout.tsx index c40d032cf..26907b375 100644 --- a/src/frontend/components/_templates/app-layout.tsx +++ b/src/frontend/components/_templates/app-layout.tsx @@ -1,6 +1,6 @@ import { ComponentPropsWithoutRef, ReactNode, useCallback, useEffect, useState } from 'react' -import { useProject, useSystem } from '../../../middleware/shared/providers' +import { useCapabilities, useProject, useSystem } from '../../../middleware/shared/providers' import { useOpenPLCStore } from '../../store' import type { RungLadderState } from '../../store/slices/ladder' import { cn } from '../../utils/cn' @@ -25,6 +25,7 @@ type AppLayoutProps = ComponentPropsWithoutRef<'main'> const AppLayout = ({ children, ...rest }: AppLayoutProps): ReactNode => { const system = useSystem() const projectPort = useProject() + const caps = useCapabilities() const [showComponent, setShowComponent] = useState(true) const modals = useOpenPLCStore(useCallback((s) => s.modals, [])) const OS = useOpenPLCStore(useCallback((s) => s.workspace.systemConfigs.OS, [])) @@ -65,7 +66,7 @@ const AppLayout = ({ children, ...rest }: AppLayoutProps): ReactNode => { return () => window.removeEventListener('resize', handleResize) }, []) - const showTitleBar = OS !== 'linux' + const showTitleBar = !caps.isNativeApplication || OS !== 'linux' return ( <> diff --git a/src/middleware/editor-platform.ts b/src/middleware/editor-platform.ts index 3a9168b9a..76c7ec040 100644 --- a/src/middleware/editor-platform.ts +++ b/src/middleware/editor-platform.ts @@ -38,10 +38,6 @@ export function setRuntimeIpAddress(ip: string): void { _runtimeIpAddress = ip } -// macOS has a native application menu bar; Windows and Linux do not -// (the Electron menu is hidden on those platforms — see menu.ts buildDefaultTemplate). -const isMac = navigator.platform.startsWith('Mac') - /** * Editor platform ports — all port interfaces wired to Electron IPC bridge. */ @@ -58,5 +54,5 @@ export const editorPorts: PlatformPorts = { accelerator: createEditorAcceleratorAdapter(), theme: createEditorThemeAdapter(), versionControl: createEditorVersionControlAdapter(), - capabilities: { ...EDITOR_CAPABILITIES, isDevMode: process.env.NODE_ENV === 'development', hasNativeMenu: isMac }, + capabilities: { ...EDITOR_CAPABILITIES, isDevMode: process.env.NODE_ENV === 'development' }, } diff --git a/src/middleware/shared/ports/platform-capabilities.ts b/src/middleware/shared/ports/platform-capabilities.ts index beee2bc44..b951bc074 100644 --- a/src/middleware/shared/ports/platform-capabilities.ts +++ b/src/middleware/shared/ports/platform-capabilities.ts @@ -13,14 +13,8 @@ export interface PlatformCapabilities { // --- Window & Chrome --- - /** True if the app has native window controls (minimize, maximize, close buttons in title bar). */ - hasNativeWindowControls: boolean - - /** True if the app has a native application menu (Electron menu bar). */ - hasNativeMenu: boolean - - /** True if the app has a native title bar with drag-to-move support (Electron frameless window). */ - hasNativeTitleBar: boolean + /** True if the app is a native desktop application (Electron editor). */ + isNativeApplication: boolean /** True if the app supports native file dialogs (open, save, pick directory). */ hasNativeFileDialogs: boolean @@ -98,9 +92,7 @@ export interface PlatformCapabilities { // --------------------------------------------------------------------------- export const EDITOR_CAPABILITIES: PlatformCapabilities = { - hasNativeWindowControls: true, - hasNativeMenu: true, - hasNativeTitleBar: true, + isNativeApplication: true, hasNativeFileDialogs: true, hasAuthentication: false, hasLocalSerialPorts: true, @@ -121,9 +113,7 @@ export const EDITOR_CAPABILITIES: PlatformCapabilities = { } export const WEB_CAPABILITIES: PlatformCapabilities = { - hasNativeWindowControls: false, - hasNativeMenu: false, - hasNativeTitleBar: false, + isNativeApplication: false, hasNativeFileDialogs: false, hasAuthentication: true, hasLocalSerialPorts: false, diff --git a/src/middleware/shared/providers/platform-context.tsx b/src/middleware/shared/providers/platform-context.tsx index a715f94c3..5ebfd0a42 100644 --- a/src/middleware/shared/providers/platform-context.tsx +++ b/src/middleware/shared/providers/platform-context.tsx @@ -9,7 +9,7 @@ import type { PlatformPorts } from './types' * // In a component: * const { compiler, runtime, capabilities } = usePlatform() * await compiler.compileProgram(args, onProgress) - * if (capabilities.hasNativeWindowControls) { ... } + * if (capabilities.isNativeApplication) { ... } * * Wiring: * // At the app root (editor):