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):