Files
pybricks-code/src/app/App.tsx
T
Laurens Valk c3a703f3b3 app: Drop doc toggle shortkey.
This was an undocumented feature that gets in the way of other shortcuts, and doesn't extend well to the sideview being used for other things in future commits.
2026-03-30 14:24:44 +02:00

176 lines
6.7 KiB
TypeScript

// SPDX-License-Identifier: MIT
// Copyright (c) 2020-2026 The Pybricks Authors
import 'react-splitter-layout/lib/index.css';
import './app.scss';
import { Classes, Spinner } from '@blueprintjs/core';
import React, { useEffect, useState } from 'react';
import SplitterLayout from 'react-splitter-layout';
import { useLocalStorage, useTernaryDarkMode } from 'usehooks-ts';
import Activities from '../activities/Activities';
import DfuWindowsDriverInstallDialog from '../firmware/dfuWindowsDriverInstallDialog/DfuWindowsDriverInstallDialog';
import { InstallPybricksDialog } from '../firmware/installPybricksDialog/InstallPybricksDialog';
import RestoreOfficialDialog from '../firmware/restoreOfficialDialog/RestoreOfficialDialog';
import { useSettingIsShowDocsEnabled } from '../settings/hooks';
import SponsorDialog from '../sponsor/SponsorDialog';
import StatusBar from '../status-bar/StatusBar';
import Toolbar from '../toolbar/Toolbar';
import Tour from '../tour/Tour';
import { docsDefaultPage } from './constants';
import { useI18n } from './i18n';
const Editor = React.lazy(async () => {
const [sagaModule, componentModule] = await Promise.all([
import('../editor/sagas'),
import('../editor/Editor'),
]);
window.dispatchEvent(
new CustomEvent('pb-lazy-saga', { detail: { saga: sagaModule.default } }),
);
return componentModule;
});
const Terminal = React.lazy(async () => {
const [sagaModule, componentModule] = await Promise.all([
import('../terminal/sagas'),
import('../terminal/Terminal'),
]);
window.dispatchEvent(
new CustomEvent('pb-lazy-saga', { detail: { saga: sagaModule.default } }),
);
return componentModule;
});
const Docs: React.FunctionComponent = () => {
return (
<iframe
onLoad={(e) => {
const target = e.target as HTMLIFrameElement;
const contentWindow = target.contentWindow;
if (!contentWindow) {
console.error('could not get iframe content window');
return;
}
if (document.body.classList.contains(Classes.DARK)) {
contentWindow.document.documentElement.classList.add(Classes.DARK);
}
}}
src={docsDefaultPage}
allowFullScreen={true}
width="100%"
height="100%"
/>
);
};
const App: React.FunctionComponent = () => {
const i18n = useI18n();
const { isDarkMode } = useTernaryDarkMode();
const { isSettingShowDocsEnabled } = useSettingIsShowDocsEnabled();
const [isDragging, setIsDragging] = useState(false);
const [docsSplit, setDocsSplit] = useLocalStorage('app-docs-split', 30);
const [terminalSplit, setTerminalSplit] = useLocalStorage('app-terminal-split', 30);
// Classes.DARK has to be applied to body element, otherwise it won't
// affect portals
useEffect(() => {
if (!isDarkMode) {
// no class for light mode, so nothing to do
return;
}
document.body.classList.add(Classes.DARK);
return () => document.body.classList.remove(Classes.DARK);
}, [isDarkMode]);
useEffect(() => {
const listener = (e: KeyboardEvent) => {
// prevent default browser keyboard shortcuts that we use
// NB: some of these like 'n' and 'w' cannot be prevented when
// running "in the browser"
if (e.ctrlKey && ['d', 'n', 's', 'w'].includes(e.key)) {
e.preventDefault();
}
};
addEventListener('keydown', listener);
return () => removeEventListener('keydown', listener);
}, []);
return (
<div className="pb-app" onContextMenu={(e) => e.preventDefault()}>
<div className="pb-app-body">
<aside
className="pb-app-activities"
aria-label={i18n.translate('landmark.activities')}
>
<Activities />
</aside>
{/* need a container with position: relative; for SplitterLayout since it uses position: absolute; */}
<div className="pb-app-main" style={{ position: 'relative' }}>
<SplitterLayout
customClassName={
isSettingShowDocsEnabled ? 'pb-show-docs' : 'pb-hide-docs'
}
onDragStart={(): void => setIsDragging(true)}
onDragEnd={(): void => setIsDragging(false)}
percentage={true}
secondaryInitialSize={docsSplit}
onSecondaryPaneSizeChange={setDocsSplit}
>
<SplitterLayout
vertical={true}
percentage={true}
secondaryInitialSize={terminalSplit}
onSecondaryPaneSizeChange={setTerminalSplit}
>
<main
className="pb-app-editor"
aria-label={i18n.translate('landmark.editor')}
>
<Toolbar />
<React.Suspense
fallback={<Spinner className="pb-editor" />}
>
<Editor />
</React.Suspense>
</main>
<aside
className="pb-app-terminal"
aria-label={i18n.translate('landmark.terminal')}
>
<React.Suspense
fallback={<Spinner className="h-100" />}
>
<Terminal />
</React.Suspense>
</aside>
</SplitterLayout>
<aside
className="pb-app-docs"
aria-label={i18n.translate('landmark.documentation')}
>
{isDragging && <div className="pb-app-docs-drag-helper" />}
<Docs />
</aside>
</SplitterLayout>
</div>
</div>
<StatusBar />
<Tour />
<DfuWindowsDriverInstallDialog />
<InstallPybricksDialog />
<RestoreOfficialDialog />
<SponsorDialog />
</div>
);
};
export default App;