Refactor Windows installer scripts and enhance Electron navigation

- Added macros for uninstalling previous Farm Control installations in the NSIS installer.
- Updated the MSI wrapped configuration to include custom actions for handling previous installations.
- Improved the Electron navigation by modifying CSS classes for better drag-and-drop functionality.
- Enhanced build scripts to include the WixUtilExtension for improved MSI packaging.
This commit is contained in:
Tom Butcher 2026-08-02 01:49:37 +01:00
parent 37fe489b07
commit f90ef10b78
8 changed files with 308 additions and 209 deletions

View File

@ -104,14 +104,8 @@
line-height: 0.7;
}
.electron-navigation-wrapper {
-webkit-app-region: drag;
}
.electron-navigation-wrapper li,
.electron-navigation-wrapper button,
.electron-navigation-wrapper .ant-tag {
-webkit-app-region: no-drag;
.electron-navigation-wrapper.electrobun-webkit-app-region-drag {
user-select: none;
}
.electron-navigation {

View File

@ -43,6 +43,7 @@ Function .onInit
${If} ${Silent}
SetAutoClose true
${EndIf}
!insertmacro uninstallPreviousFarmControl
FunctionEnd
Section "Farm Control" SecMain

View File

@ -1,3 +1,33 @@
!macro quitFarmControl
DetailPrint "Stopping running Farm Control processes..."
ExecWait 'taskkill /F /IM launcher.exe /T' $R0
!macroend
!macro uninstallPreviousFarmControl
ClearErrors
ReadRegStr $R0 HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Farm Control" "UninstallString"
StrCmp $R0 "" check_install_dir run_uninstall
check_install_dir:
ReadRegStr $R0 HKLM "Software\Tom Butcher\Farm Control" "InstallDir"
StrCmp $R0 "" check_default_dir
StrCpy $R0 "$R0\Uninstall.exe"
Goto run_uninstall
check_default_dir:
StrCpy $R0 "$PROGRAMFILES64\Farm Control\Uninstall.exe"
IfFileExists $R0 run_uninstall done_uninstall
run_uninstall:
IfFileExists $R0 0 done_uninstall
DetailPrint "Removing previous Farm Control installation..."
!insertmacro quitFarmControl
ExecWait '$R0 /S' $R1
DetailPrint "Previous installation removed (exit code: $R1)"
done_uninstall:
!macroend
!macro createDesktopShortcut
CreateShortCut "$DESKTOP\Farm Control.lnk" "$INSTDIR\bin\launcher.exe"
!macroend

View File

@ -23,8 +23,42 @@
<Property Id="DISABLEADVTSHORTCUTS" Value="1" />
<Property Id="MSIINSTALLPERUSER" Value="1" />
<Property Id="PREVIOUS_UNINSTALL_CMD">
<RegistrySearch
Id="PreviousUninstallCmdSearch"
Root="HKLM"
Key="Software\Microsoft\Windows\CurrentVersion\Uninstall\Farm Control"
Name="UninstallString"
Type="raw" />
</Property>
<Property Id="PREVIOUS_INSTALL_DIR">
<RegistrySearch
Id="PreviousInstallDirSearch"
Root="HKLM"
Key="Software\Tom Butcher\Farm Control"
Name="InstallDir"
Type="directory" />
</Property>
<Binary Id="WrappedExe" SourceFile="__SETUP_EXE__" />
<CustomAction
Id="KillFarmControl"
BinaryKey="WixCA"
DllEntry="WixQuietExec"
Execute="deferred"
Impersonate="no"
Return="ignore" />
<CustomAction
Id="UninstallPrevious"
BinaryKey="WixCA"
DllEntry="WixQuietExec"
Execute="deferred"
Impersonate="no"
Return="ignore" />
<CustomAction
Id="RunInstaller"
BinaryKey="WrappedExe"
@ -45,8 +79,33 @@
<ComponentRef Id="EmptyComponent" />
</Feature>
<CustomAction
Id="SetKillFarmControl"
Property="QtKillFarmControl"
Value="taskkill.exe /F /IM launcher.exe /T"
Execute="immediate" />
<CustomAction
Id="SetUninstallPreviousFromRegistry"
Property="QtUninstallPrevious"
Value="&quot;[PREVIOUS_UNINSTALL_CMD]&quot; /S"
Execute="immediate" />
<CustomAction
Id="SetUninstallPreviousFromInstallDir"
Property="QtUninstallPrevious"
Value="&quot;[PREVIOUS_INSTALL_DIR]Uninstall.exe&quot; /S"
Execute="immediate" />
<InstallExecuteSequence>
<Custom Action="RunInstaller" After="ProcessComponents">NOT Installed</Custom>
<Custom Action="SetKillFarmControl" Before="KillFarmControl">NOT Installed</Custom>
<Custom Action="KillFarmControl" After="SetKillFarmControl">NOT Installed</Custom>
<Custom Action="SetUninstallPreviousFromRegistry" After="KillFarmControl">NOT Installed AND PREVIOUS_UNINSTALL_CMD</Custom>
<Custom Action="SetUninstallPreviousFromInstallDir" After="KillFarmControl">NOT Installed AND NOT PREVIOUS_UNINSTALL_CMD AND PREVIOUS_INSTALL_DIR</Custom>
<Custom Action="UninstallPrevious" After="SetUninstallPreviousFromRegistry">NOT Installed AND (PREVIOUS_UNINSTALL_CMD OR PREVIOUS_INSTALL_DIR)</Custom>
<Custom Action="UninstallPrevious" After="SetUninstallPreviousFromInstallDir">NOT Installed AND (PREVIOUS_UNINSTALL_CMD OR PREVIOUS_INSTALL_DIR)</Custom>
<Custom Action="RunInstaller" After="UninstallPrevious">NOT Installed AND (PREVIOUS_UNINSTALL_CMD OR PREVIOUS_INSTALL_DIR)</Custom>
<Custom Action="RunInstaller" After="KillFarmControl">NOT Installed AND NOT PREVIOUS_UNINSTALL_CMD AND NOT PREVIOUS_INSTALL_DIR</Custom>
</InstallExecuteSequence>
</Product>
</Wix>

View File

@ -103,12 +103,12 @@ $wixObj = Join-Path $workDir "project.wixobj"
Push-Location $workDir
try {
& $wix.Candle "-arch" "x64" $projectWxs
& $wix.Candle "-arch" "x64" "-ext" "WixUtilExtension" $projectWxs
if ($LASTEXITCODE -ne 0) {
throw "candle.exe failed with exit code $LASTEXITCODE"
}
& $wix.Light "-out" $outputMsiPath "-spdb" "-sw1076" $wixObj
& $wix.Light "-out" $outputMsiPath "-spdb" "-sw1076" "-ext" "WixUtilExtension" $wixObj
if ($LASTEXITCODE -ne 0) {
throw "light.exe failed with exit code $LASTEXITCODE"
}

View File

@ -3,34 +3,34 @@ import {
existsSync,
mkdirSync,
readFileSync,
writeFileSync,
} from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
writeFileSync
} from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const electrobunDir = path.join(rootDir, "node_modules/electrobun");
const sharedSrcDir = path.join(electrobunDir, "src/shared");
const sharedDistDir = path.join(electrobunDir, "dist/api/shared");
const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
const electrobunDir = path.join(rootDir, 'node_modules/electrobun')
const sharedSrcDir = path.join(electrobunDir, 'src/shared')
const sharedDistDir = path.join(electrobunDir, 'dist/api/shared')
if (!existsSync(sharedDistDir)) {
console.error("patch-electrobun-src: electrobun dist/api/shared not found");
process.exit(1);
console.error('patch-electrobun-src: electrobun dist/api/shared not found')
process.exit(1)
}
mkdirSync(sharedSrcDir, { recursive: true });
mkdirSync(sharedSrcDir, { recursive: true })
for (const fileName of [
"cef-version.ts",
"bun-version.ts",
"electrobun-version.ts",
"naming.ts",
"rpc.ts",
'cef-version.ts',
'bun-version.ts',
'electrobun-version.ts',
'naming.ts',
'rpc.ts'
]) {
const source = path.join(sharedDistDir, fileName);
const destination = path.join(sharedSrcDir, fileName);
const source = path.join(sharedDistDir, fileName)
const destination = path.join(sharedSrcDir, fileName)
if (existsSync(source)) {
cpSync(source, destination);
cpSync(source, destination)
}
}
@ -109,14 +109,14 @@ export function getPlatformOS(): SupportedOS {
export function getPlatformArch(): SupportedArch {
return ARCH;
}
`;
`
writeFileSync(path.join(sharedSrcDir, "platform.ts"), platformPatch);
writeFileSync(path.join(sharedSrcDir, 'platform.ts'), platformPatch)
const templatesDir = path.join(electrobunDir, "src/cli/templates");
mkdirSync(templatesDir, { recursive: true });
const templatesDir = path.join(electrobunDir, 'src/cli/templates')
mkdirSync(templatesDir, { recursive: true })
writeFileSync(
path.join(templatesDir, "embedded.ts"),
path.join(templatesDir, 'embedded.ts'),
`export function getTemplateNames() {
return [];
}
@ -124,8 +124,8 @@ writeFileSync(
export function getTemplate(name: string) {
throw new Error(\`Template not available in patched electrobun CLI: \${name}\`);
}
`,
);
`
)
const RCEDIT_RESOLVER_FN = `function resolveProjectRceditPkgPath(projectRoot) {
const candidates = [
@ -140,35 +140,41 @@ const RCEDIT_RESOLVER_FN = `function resolveProjectRceditPkgPath(projectRoot) {
throw new Error(
"rcedit not found under " + projectRoot + ". Install rcedit in the project (bun add -d rcedit).",
);
}`;
}`
const cliPath = path.join(electrobunDir, "src/cli/index.ts");
let cliSource = readFileSync(cliPath, "utf8");
if (!cliSource.includes("HOST_ARCH")) {
const cliPath = path.join(electrobunDir, 'src/cli/index.ts')
let cliSource = readFileSync(cliPath, 'utf8')
if (!cliSource.includes('HOST_ARCH')) {
cliSource = cliSource.replace(
'import { OS, ARCH } from "../shared/platform";',
'import { OS, ARCH, HOST_ARCH } from "../shared/platform";',
);
'import { OS, ARCH, HOST_ARCH } from "../shared/platform";'
)
cliSource = cliSource.replace(
"const hostPaths = getPlatformPaths(OS, ARCH);",
"const hostPaths = getPlatformPaths(OS, HOST_ARCH);",
);
'const hostPaths = getPlatformPaths(OS, ARCH);',
'const hostPaths = getPlatformPaths(OS, HOST_ARCH);'
)
}
if (!cliSource.includes("toolPaths")) {
if (!cliSource.includes('toolPaths')) {
cliSource = cliSource.replace(
"const targetPaths = getPlatformPaths(currentTarget.os, currentTarget.arch);",
"const targetPaths = getPlatformPaths(currentTarget.os, currentTarget.arch);\n\t\tconst toolPaths = getPlatformPaths(currentTarget.os, ARCH !== HOST_ARCH ? HOST_ARCH : ARCH);",
);
cliSource = cliSource.replaceAll("const zstdPath = targetPaths.ZSTD;", "const zstdPath = toolPaths.ZSTD;");
cliSource = cliSource.replaceAll("const bsdiffpath = targetPaths.BSDIFF;", "const bsdiffpath = toolPaths.BSDIFF;");
'const targetPaths = getPlatformPaths(currentTarget.os, currentTarget.arch);',
'const targetPaths = getPlatformPaths(currentTarget.os, currentTarget.arch);\n\t\tconst toolPaths = getPlatformPaths(currentTarget.os, ARCH !== HOST_ARCH ? HOST_ARCH : ARCH);'
)
cliSource = cliSource.replaceAll(
'const zstdPath = targetPaths.ZSTD;',
'const zstdPath = toolPaths.ZSTD;'
)
cliSource = cliSource.replaceAll(
'const bsdiffpath = targetPaths.BSDIFF;',
'const bsdiffpath = toolPaths.BSDIFF;'
)
cliSource = cliSource.replace(
"zigAsarCli = join(targetPaths.BSPATCH).replace(\"bspatch\", \"zig-asar\");",
"zigAsarCli = join(toolPaths.BSPATCH).replace(\"bspatch\", \"zig-asar\");",
);
writeFileSync(cliPath, cliSource);
cliSource = readFileSync(cliPath, "utf8");
'zigAsarCli = join(targetPaths.BSPATCH).replace("bspatch", "zig-asar");',
'zigAsarCli = join(toolPaths.BSPATCH).replace("bspatch", "zig-asar");'
)
writeFileSync(cliPath, cliSource)
cliSource = readFileSync(cliPath, 'utf8')
}
if (!cliSource.includes("hookBunBinary")) {
if (!cliSource.includes('hookBunBinary')) {
cliSource = cliSource.replace(
`const hostPaths = getPlatformPaths(OS, HOST_ARCH);
@ -178,41 +184,41 @@ if (!cliSource.includes("hookBunBinary")) {
? hostPaths.BUN_BINARY
: process.execPath;
const result = Bun.spawnSync([hookBunBinary, hookScript],`,
);
const result = Bun.spawnSync([hookBunBinary, hookScript],`
)
cliSource = cliSource.replace(
"console.error(\"Tried to run with bun at:\", hostPaths.BUN_BINARY);",
"console.error(\"Tried to run with bun at:\", hookBunBinary);",
);
writeFileSync(cliPath, cliSource);
cliSource = readFileSync(cliPath, "utf8");
'console.error("Tried to run with bun at:", hostPaths.BUN_BINARY);',
'console.error("Tried to run with bun at:", hookBunBinary);'
)
writeFileSync(cliPath, cliSource)
cliSource = readFileSync(cliPath, 'utf8')
}
if (!cliSource.includes("resolveProjectRceditPkgPath")) {
if (!cliSource.includes('resolveProjectRceditPkgPath')) {
cliSource = cliSource.replaceAll(
"const rceditPkgPath = require.resolve(\"rcedit/package.json\");",
"const rceditPkgPath = resolveProjectRceditPkgPath(projectRoot);",
);
'const rceditPkgPath = require.resolve("rcedit/package.json");',
'const rceditPkgPath = resolveProjectRceditPkgPath(projectRoot);'
)
cliSource = cliSource.replace(
"function getPlatformPaths(",
'function getPlatformPaths(',
`${RCEDIT_RESOLVER_FN}
function getPlatformPaths(`,
);
writeFileSync(cliPath, cliSource);
cliSource = readFileSync(cliPath, "utf8");
function getPlatformPaths(`
)
writeFileSync(cliPath, cliSource)
cliSource = readFileSync(cliPath, 'utf8')
} else {
cliSource = cliSource.replace(
/function resolveProjectRceditPkgPath\(projectRoot\) \{[\s\S]*?\n\}/m,
RCEDIT_RESOLVER_FN,
);
writeFileSync(cliPath, cliSource);
RCEDIT_RESOLVER_FN
)
writeFileSync(cliPath, cliSource)
}
const electrobunCjsPath = path.join(electrobunDir, "bin/electrobun.cjs");
let electrobunCjs = readFileSync(electrobunCjsPath, "utf8");
const electrobunCjsPath = path.join(electrobunDir, 'bin/electrobun.cjs')
let electrobunCjs = readFileSync(electrobunCjsPath, 'utf8')
if (!electrobunCjs.includes("farmcontrol-use-patched-cli")) {
if (!electrobunCjs.includes('farmcontrol-use-patched-cli')) {
electrobunCjs = electrobunCjs.replace(
`async function main() {
try {
@ -259,19 +265,21 @@ if (!electrobunCjs.includes("farmcontrol-use-patched-cli")) {
const child = spawn(cliPath, args, {
stdio: 'inherit',
cwd: process.cwd()
});`,
);
writeFileSync(electrobunCjsPath, electrobunCjs);
});`
)
writeFileSync(electrobunCjsPath, electrobunCjs)
}
const rceditSrc = path.join(rootDir, "node_modules/rcedit");
const rceditDest = path.join(electrobunDir, "node_modules/rcedit");
const rceditSrc = path.join(rootDir, 'node_modules/rcedit')
const rceditDest = path.join(electrobunDir, 'node_modules/rcedit')
if (existsSync(rceditSrc)) {
mkdirSync(path.join(electrobunDir, "node_modules"), { recursive: true });
cpSync(rceditSrc, rceditDest, { recursive: true, force: true });
mkdirSync(path.join(electrobunDir, 'node_modules'), { recursive: true })
cpSync(rceditSrc, rceditDest, { recursive: true, force: true })
}
const markerPath = path.join(electrobunDir, ".farmcontrol-electrobun-patched");
writeFileSync(markerPath, `patched-at=${new Date().toISOString()}\n`);
const markerPath = path.join(electrobunDir, '.farmcontrol-electrobun-patched')
writeFileSync(markerPath, `patched-at=${new Date().toISOString()}\n`)
console.log("patch-electrobun-src: electrobun src/shared ready for cross-arch builds");
console.log(
'patch-electrobun-src: electrobun src/shared ready for cross-arch builds'
)

View File

@ -199,134 +199,138 @@ const DashboardNavigation = () => {
</Text>
</Flex>
)}
<Menu
mode='horizontal'
className={isElectron ? 'electron-navigation' : null}
items={mainMenuItems}
style={{
flexWrap: 'wrap',
flexGrow: isMobile ? 0 : 1,
border: 0,
width: isMobile ? '64px' : 'unset'
}}
onClick={handleMainMenuClick}
selectedKeys={[selectedKey]}
overflowedIndicator={
<Button
type='text'
icon={<MenuIcon />}
style={{ marginBottom: '4px' }}
/>
}
/>
{isMobile && <div style={{ flexGrow: 1 }} />}
<Flex
gap={'small'}
align='center'
style={{ marginTop: '-2px', marginRight: '6px' }}
>
<Space style={{ paddingTop: '2px', marginRight: '8px' }}>
<WebAppSwitcher />
<KeyboardShortcut
shortcut='alt+q'
hint='ALT Q'
onTrigger={() => showSpotlight()}
>
<div className='electrobun-webkit-app-region-no-drag'>
<Menu
mode='horizontal'
className={isElectron ? 'electron-navigation' : null}
items={mainMenuItems}
style={{
flexWrap: 'wrap',
flexGrow: isMobile ? 0 : 1,
border: 0,
width: isMobile ? '64px' : 'unset'
}}
onClick={handleMainMenuClick}
selectedKeys={[selectedKey]}
overflowedIndicator={
<Button
icon={<SearchIcon />}
type='text'
style={{ marginTop: '4px' }}
onClick={() => showSpotlight()}
icon={<MenuIcon />}
style={{ marginBottom: '4px' }}
/>
</KeyboardShortcut>
<Badge
count={unreadCount}
size='small'
offset={[-5, 8]}
style={{ padding: 0, fontWeight: 600 }}
>
}
/>
</div>
{isMobile && <div style={{ flexGrow: 1 }} />}
<div className='electrobun-webkit-app-region-no-drag'>
<Flex
gap={'small'}
align='center'
style={{ marginTop: '-2px', marginRight: '6px' }}
>
<Space style={{ paddingTop: '2px', marginRight: '8px' }}>
<WebAppSwitcher />
<KeyboardShortcut
shortcut='alt+n'
hint='ALT N'
onTrigger={() => toggleNotificationCenter()}
shortcut='alt+q'
hint='ALT Q'
onTrigger={() => showSpotlight()}
>
<Button
icon={<BellIcon />}
icon={<SearchIcon />}
type='text'
style={{ marginTop: '2px' }}
onClick={() => toggleNotificationCenter()}
style={{ marginTop: '4px' }}
onClick={() => showSpotlight()}
/>
</KeyboardShortcut>
</Badge>
</Space>
{import.meta.env.MODE === 'development' && (
<Space>
{apiServerState === 'connected' ? (
<Tooltip title='Connected to api server' arrow={false}>
<Tag
color='success'
style={{ marginRight: 0 }}
icon={<CloudIcon />}
/>
</Tooltip>
) : null}
{apiServerState === 'connecting' ? (
<Tooltip title='Connecting to api erver...' arrow={false}>
<Tag
color='warning'
style={{ marginRight: 0 }}
icon={<LoadingOutlined />}
/>
</Tooltip>
) : null}
{apiServerState === 'disconnected' ? (
<Tooltip title='Disconnected from api server' arrow={false}>
<Tag
color='error'
style={{ marginRight: 0 }}
icon={<CloudIcon />}
/>
</Tooltip>
) : null}
<Tooltip title='Developer' arrow={false}>
<Tag
color='yellow'
style={{ marginRight: 0 }}
icon={<DeveloperIcon />}
onClick={() => {
navigate('/dashboard/developer/sessionstorage')
}}
/>
</Tooltip>
</Space>
)}
{userProfile ? (
<Space>
<Popover
content={userPopoverContent}
placement='bottomRight'
trigger='hover'
open={userPopoverOpen}
onOpenChange={setUserPopoverOpen}
arrow={false}
<Badge
count={unreadCount}
size='small'
offset={[-5, 8]}
style={{ padding: 0, fontWeight: 600 }}
>
<Tag style={{ marginRight: 0 }} icon={<PersonIcon />}>
{!isMobile && (userProfile?.name || userProfile.username)}
</Tag>
</Popover>
<KeyboardShortcut
shortcut='alt+n'
hint='ALT N'
onTrigger={() => toggleNotificationCenter()}
>
<Button
icon={<BellIcon />}
type='text'
style={{ marginTop: '2px' }}
onClick={() => toggleNotificationCenter()}
/>
</KeyboardShortcut>
</Badge>
</Space>
) : null}
{isElectron && availableUpdate ? (
<Tag
icon={<CloudIcon />}
style={{ cursor: 'pointer', margin: '2px 0 0 0' }}
color='cyan'
onClick={() => checkForUpdates()}
>
Update Available
</Tag>
) : null}
</Flex>
{import.meta.env.MODE === 'development' && (
<Space>
{apiServerState === 'connected' ? (
<Tooltip title='Connected to api server' arrow={false}>
<Tag
color='success'
style={{ marginRight: 0 }}
icon={<CloudIcon />}
/>
</Tooltip>
) : null}
{apiServerState === 'connecting' ? (
<Tooltip title='Connecting to api erver...' arrow={false}>
<Tag
color='warning'
style={{ marginRight: 0 }}
icon={<LoadingOutlined />}
/>
</Tooltip>
) : null}
{apiServerState === 'disconnected' ? (
<Tooltip title='Disconnected from api server' arrow={false}>
<Tag
color='error'
style={{ marginRight: 0 }}
icon={<CloudIcon />}
/>
</Tooltip>
) : null}
<Tooltip title='Developer' arrow={false}>
<Tag
color='yellow'
style={{ marginRight: 0 }}
icon={<DeveloperIcon />}
onClick={() => {
navigate('/dashboard/developer/sessionstorage')
}}
/>
</Tooltip>
</Space>
)}
{userProfile ? (
<Space>
<Popover
content={userPopoverContent}
placement='bottomRight'
trigger='hover'
open={userPopoverOpen}
onOpenChange={setUserPopoverOpen}
arrow={false}
>
<Tag style={{ marginRight: 0 }} icon={<PersonIcon />}>
{!isMobile && (userProfile?.name || userProfile.username)}
</Tag>
</Popover>
</Space>
) : null}
{isElectron && availableUpdate ? (
<Tag
icon={<CloudIcon />}
style={{ cursor: 'pointer', margin: '2px 0 0 0' }}
color='cyan'
onClick={() => checkForUpdates()}
>
Update Available
</Tag>
) : null}
</Flex>
</div>
{isOtherApp ? <DashboardWindowButtons /> : null}
</Flex>
)
@ -335,7 +339,7 @@ const DashboardNavigation = () => {
<>
{isElectron ? (
<Flex
className={`ant-menu-horizontal ant-menu-light${!notificationCenterVisible ? ' electron-navigation-wrapper' : ''}`}
className={`ant-menu-horizontal ant-menu-light${!notificationCenterVisible ? ' electron-navigation-wrapper electrobun-webkit-app-region-drag' : ''}`}
style={{ lineHeight: '40px', padding: '0 2px 0 2px' }}
>
{navigationContents}

View File

@ -39,7 +39,10 @@ const DashboardWindowButtons = () => {
<div style={{ width: '80px' }} />
) : null
) : (
<div style={{ width: '95px', marginRight: '2.5px' }}>
<div
className='electrobun-webkit-app-region-no-drag'
style={{ width: '95px', marginRight: '2.5px' }}
>
<Flex style={{ position: 'relative', zIndex: 9999 }}>
{maximizeButton}
{minimizeButton}