+
{({ isVisible }: { isVisible: boolean }) => {
if (!isVisible) return ...
;
return (
@@ -120,14 +102,12 @@ const SequencePlotChild: FunctionComponent = ({
};
type ExpandComponentProps = {
- width: number;
expanded: boolean;
setExpanded: (expanded: boolean) => void;
label: string;
};
const ExpandComponent: FunctionComponent = ({
- width,
expanded,
setExpanded,
label,
@@ -135,7 +115,6 @@ const ExpandComponent: FunctionComponent = ({
return (
setExpanded(!expanded)}
>
{expanded ? "▼" : "▶"}
diff --git a/gui/src/app/SamplingOptsPanel/SamplingOptsPanel.tsx b/gui/src/app/SamplingOptsPanel/SamplingOptsPanel.tsx
index 8c348643..70f236e6 100644
--- a/gui/src/app/SamplingOptsPanel/SamplingOptsPanel.tsx
+++ b/gui/src/app/SamplingOptsPanel/SamplingOptsPanel.tsx
@@ -1,5 +1,5 @@
-import { Hyperlink } from "@fi-sci/misc";
-import { Grid } from "@mui/material";
+import Grid from "@mui/material/Grid";
+import Link from "@mui/material/Link";
import { defaultSamplingOpts, SamplingOpts } from "@SpCore/ProjectDataModel";
import { FunctionComponent, useCallback } from "react";
@@ -167,9 +167,14 @@ const SamplingOptsPanel: FunctionComponent
= ({
-
+
reset
-
+
);
diff --git a/gui/src/app/StatusBar.tsx b/gui/src/app/StatusBar.tsx
deleted file mode 100644
index 5cf6f9f2..00000000
--- a/gui/src/app/StatusBar.tsx
+++ /dev/null
@@ -1,77 +0,0 @@
-/* eslint-disable @typescript-eslint/no-explicit-any */
-import {
- createContext,
- FunctionComponent,
- useCallback,
- useContext,
-} from "react";
-
-type Props = {
- width: number;
- height: number;
-};
-
-export const connectedColor = "#050";
-export const notConnectedColor = "#888";
-
-export const statusBarHeight = 18;
-// export const statusBarHeight = 0
-
-type CustomStatusBarElements = {
- [key: string]: any;
-};
-type CustomStatusBarAction = {
- type: "set";
- key: string;
- value: string;
-};
-export const customStatusBarElementsReducer = (
- state: CustomStatusBarElements,
- action: CustomStatusBarAction,
-): CustomStatusBarElements => {
- if (action.type === "set") {
- if (state[action.key] === action.value) return state; // this is important to avoid unnecessary re-renders
- return {
- ...state,
- [action.key]: action.value,
- };
- } else return state;
-};
-export const CustomStatusBarElementsContext = createContext<{
- customStatusBarElements: CustomStatusBarElements;
- customStatusBarElementsDispatch: (action: CustomStatusBarAction) => void;
-} | null>(null);
-
-export const useCustomStatusBarElements = () => {
- const { customStatusBarElements, customStatusBarElementsDispatch } =
- useContext(CustomStatusBarElementsContext) || {};
- const setCustomStatusBarElement = useCallback(
- (key: string, value: any) => {
- customStatusBarElementsDispatch &&
- customStatusBarElementsDispatch({ type: "set", key, value });
- },
- [customStatusBarElementsDispatch],
- );
- return {
- customStatusBarElements: customStatusBarElements || {},
- setCustomStatusBarElement,
- };
-};
-
-const StatusBar: FunctionComponent = () => {
- const { customStatusBarElements } = useCustomStatusBarElements();
- return (
-
- {/* The following is flush right */}
-
- {Object.keys(customStatusBarElements || {}).map((key) => (
-
- {(customStatusBarElements || {})[key]}
-
- ))}
-
-
- );
-};
-
-export default StatusBar;
diff --git a/gui/src/app/TabWidget/TabWidget.tsx b/gui/src/app/TabWidget/TabWidget.tsx
deleted file mode 100644
index d832364c..00000000
--- a/gui/src/app/TabWidget/TabWidget.tsx
+++ /dev/null
@@ -1,118 +0,0 @@
-/* eslint-disable @typescript-eslint/no-explicit-any */
-import TabWidgetTabBar from "@SpComponents/TabWidgetTabBar";
-import {
- FunctionComponent,
- PropsWithChildren,
- useCallback,
- useEffect,
- useMemo,
- useState,
-} from "react";
-
-type Props = {
- tabs: {
- id: string;
- label: string;
- title?: string;
- closeable: boolean;
- icon?: any;
- }[];
- currentTabId: string | undefined;
- setCurrentTabId: (id: string) => void;
- onCloseTab?: (id: string) => void;
- width: number;
- height: number;
-};
-
-const tabBarHeight = 30;
-
-const TabWidget: FunctionComponent> = ({
- children,
- tabs,
- currentTabId,
- setCurrentTabId,
- onCloseTab,
- width,
- height,
-}) => {
- const currentTabIndex = useMemo(() => {
- if (!currentTabId) return undefined;
- const index = tabs.findIndex((t) => t.id === currentTabId);
- if (index < 0) return undefined;
- return index;
- }, [currentTabId, tabs]);
- const children2 = Array.isArray(children)
- ? (children as React.ReactElement[])
- : ([children] as React.ReactElement[]);
- if ((children2 || []).length !== tabs.length) {
- throw Error(
- `TabWidget: incorrect number of tabs ${(children2 || []).length} <> ${tabs.length}`,
- );
- }
- const hMargin = 8;
- const vMargin = 8;
- const W = (width || 300) - hMargin * 2;
- const H = height - vMargin * 2;
- const [hasBeenVisible, setHasBeenVisible] = useState([]);
- useEffect(() => {
- if (currentTabIndex === undefined) return;
- if (!hasBeenVisible.includes(currentTabIndex)) {
- setHasBeenVisible([...hasBeenVisible, currentTabIndex]);
- }
- }, [currentTabIndex, hasBeenVisible]);
- const handleSetCurrentTabIndex = useCallback(
- (index: number) => {
- if (index < 0 || index >= tabs.length) return;
- setCurrentTabId(tabs[index].id);
- },
- [setCurrentTabId, tabs],
- );
- return (
-
-
-
-
- {children2.map((c, i) => {
- const visible = i === currentTabIndex;
- return (
-
- {(visible || hasBeenVisible.includes(i)) && (
-
- )}
-
- );
- })}
-
- );
-};
-
-export default TabWidget;
diff --git a/gui/src/app/TabWidget/TabWidgetTabBar.tsx b/gui/src/app/TabWidget/TabWidgetTabBar.tsx
deleted file mode 100644
index 77c85b39..00000000
--- a/gui/src/app/TabWidget/TabWidgetTabBar.tsx
+++ /dev/null
@@ -1,71 +0,0 @@
-/* eslint-disable @typescript-eslint/no-explicit-any */
-import { Tab, Tabs } from "@mui/material";
-import { FunctionComponent, useEffect } from "react";
-
-type Props = {
- tabs: {
- id: string;
- label: string;
- title?: string;
- closeable: boolean;
- icon?: any;
- }[];
- currentTabIndex: number | undefined;
- onCurrentTabIndexChanged: (i: number) => void;
- onCloseTab?: (id: string) => void;
-};
-
-const TabWidgetTabBar: FunctionComponent = ({
- tabs,
- currentTabIndex,
- onCurrentTabIndexChanged,
- onCloseTab,
-}) => {
- useEffect(() => {
- if (currentTabIndex === undefined) {
- if (tabs.length > 0) {
- onCurrentTabIndexChanged(0);
- }
- }
- }, [currentTabIndex, onCurrentTabIndexChanged, tabs.length]);
- return (
- {
- onCurrentTabIndexChanged(value);
- }}
- >
- {tabs.map((tab, i) => (
-
- {tab.icon ? (
- {tab.icon}
- ) : (
-
- )}
- {tab.label}
-
- {tab.closeable && onCloseTab && (
- {
- onCloseTab(tab.id);
- }}
- >
- ✕
-
- )}
-
- }
- sx={{ minHeight: 0, height: 0, fontSize: 12 }}
- />
- ))}
-
- );
-};
-
-export default TabWidgetTabBar;
diff --git a/gui/src/app/components/LazyPlotlyPlot.tsx b/gui/src/app/components/LazyPlotlyPlot.tsx
index 7cb805f5..d4f1c6d8 100644
--- a/gui/src/app/components/LazyPlotlyPlot.tsx
+++ b/gui/src/app/components/LazyPlotlyPlot.tsx
@@ -1,17 +1,36 @@
+import CircularProgress from "@mui/material/CircularProgress";
import React, { FunctionComponent, Suspense } from "react";
import type { PlotParams } from "react-plotly.js";
import createPlotlyComponent from "react-plotly.js/factory";
+import useMeasure from "react-use-measure";
const Plot = React.lazy(async () => {
const plotly = await import("plotly.js-cartesian-dist");
return { default: createPlotlyComponent(plotly) };
});
const LazyPlotlyPlot: FunctionComponent = ({ data, layout }) => {
+ // plotly has a reactive setting, but it is buggy
+ const [ref, { width }] = useMeasure();
+
+ const layoutWithWidth = {
+ ...layout,
+ width: width,
+ };
+
return (
- Loading plotly }>
-
-
+
+
+
+ Loading Plotly.js
+ >
+ }
+ >
+
+
+
);
};
diff --git a/gui/src/app/components/ResponsiveGrid.tsx b/gui/src/app/components/ResponsiveGrid.tsx
new file mode 100644
index 00000000..b703f3cc
--- /dev/null
+++ b/gui/src/app/components/ResponsiveGrid.tsx
@@ -0,0 +1,40 @@
+// by default, MUI grids are only responsive to the viewport width
+// not their container. This is based on the solution in
+// https://github.com/mui/material-ui/issues/25189#issuecomment-1321236185
+
+import { styled } from "@mui/material/styles";
+import { FunctionComponent } from "react";
+
+const Container = styled("div")({
+ display: "flex",
+ flexWrap: "wrap",
+ gap: 4,
+ containerType: "inline-size",
+});
+
+const Item = styled("div")(({ theme }) => ({
+ width: "95%",
+ margin: "0 auto",
+ [theme.breakpoints.up("md").replace("@media", "@container")]: {
+ width: "calc(50% - 4px)",
+ },
+ [theme.breakpoints.up("lg").replace("@media", "@container")]: {
+ width: "calc(100% / 4 - 12px)",
+ },
+}));
+
+type Props = {
+ children: JSX.Element[];
+};
+
+const ResponsiveGrid: FunctionComponent
= ({ children }) => {
+ return (
+
+ {children.map((child, i) => (
+ - {child}
+ ))}
+
+ );
+};
+
+export default ResponsiveGrid;
diff --git a/gui/src/app/components/Splitter.tsx b/gui/src/app/components/Splitter.tsx
new file mode 100644
index 00000000..d167b3f3
--- /dev/null
+++ b/gui/src/app/components/Splitter.tsx
@@ -0,0 +1,62 @@
+// @devbookhq/splitter has a known issue, https://github.com/DevbookHQ/splitter/issues/11,
+// where re-renders of internal components can cause the splitter to lose its state.
+// This wrapper captures the splitter sizes and stores them in a state to avoid this issue.
+import DevbookSplit, {
+ SplitDirection as DevbookSplitDirection,
+ GutterTheme as DevbookGutterTheme,
+} from "@devbookhq/splitter";
+import {
+ FunctionComponent,
+ useState,
+ useCallback,
+ PropsWithChildren,
+ useEffect,
+} from "react";
+
+interface SplitterProps {
+ direction?: keyof typeof DevbookSplitDirection;
+ minWidths?: number[];
+ minHeights?: number[];
+ initialSizes?: number[];
+ gutterTheme?: keyof typeof DevbookGutterTheme;
+ gutterClassName?: string;
+ draggerClassName?: string;
+ onResizeStarted?: (pairIdx: number) => void;
+ onResizeFinished?: (pairIdx: number, newSizes: number[]) => void;
+ classes?: string[];
+}
+
+export const SplitDirection = DevbookSplitDirection;
+export const GutterTheme = DevbookGutterTheme;
+
+export const Splitter: FunctionComponent> = ({
+ direction = "Horizontal",
+ gutterTheme = "Light",
+ children,
+ initialSizes,
+ ...props
+}) => {
+ const [persistentSizes, setPersistentSizes] = useState(
+ initialSizes,
+ );
+
+ useEffect(() => {
+ setPersistentSizes(initialSizes);
+ }, [initialSizes]);
+
+ const handleResizeFinished = useCallback((_: number, newSizes: number[]) => {
+ setPersistentSizes(newSizes);
+ }, []);
+
+ return (
+
+ {children}
+
+ );
+};
diff --git a/gui/src/app/components/TabWidget.tsx b/gui/src/app/components/TabWidget.tsx
new file mode 100644
index 00000000..e2ed7717
--- /dev/null
+++ b/gui/src/app/components/TabWidget.tsx
@@ -0,0 +1,64 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+import { FunctionComponent, useState } from "react";
+import Tabs from "@mui/material/Tabs";
+import Tab from "@mui/material/Tab";
+import Box from "@mui/material/Box";
+
+type TabWidgetProps = {
+ labels: string[];
+ children: JSX.Element[];
+};
+
+const TabWidget: FunctionComponent = ({ labels, children }) => {
+ if (labels.length !== children.length) {
+ throw new Error("Number of labels and children must match");
+ }
+
+ const [index, setIndex] = useState(0);
+
+ const handleChange = (_: React.SyntheticEvent, newValue: number) => {
+ setIndex(newValue);
+ };
+
+ return (
+
+
+ {labels.map((label, i) => (
+
+ ))}
+
+
+ {children.map((child, i) => (
+
+ {child}
+
+ ))}
+
+
+ );
+};
+
+interface TabPanelProps {
+ children?: React.ReactNode;
+ index: number;
+ value: number;
+}
+
+const CustomTabPanel = (props: TabPanelProps) => {
+ const { children, value, index, ...other } = props;
+
+ return (
+
+ {value === index && <>{children}>}
+
+ );
+};
+
+export default TabWidget;
diff --git a/gui/src/app/pages/HomePage/HomePage.tsx b/gui/src/app/pages/HomePage/HomePage.tsx
index ae6ca11f..84ae8086 100644
--- a/gui/src/app/pages/HomePage/HomePage.tsx
+++ b/gui/src/app/pages/HomePage/HomePage.tsx
@@ -1,19 +1,22 @@
-import { Splitter } from "@fi-sci/splitter";
+import Box from "@mui/material/Box";
+import Divider from "@mui/material/Divider";
+import Grid from "@mui/material/Grid";
+import styled from "@mui/material/styles/styled";
+import useMediaQuery from "@mui/material/useMediaQuery";
import DataFileEditor from "@SpComponents/DataFileEditor";
import RunPanel from "@SpComponents/RunPanel";
import SamplerOutputView from "@SpComponents/SamplerOutputView";
import SamplingOptsPanel from "@SpComponents/SamplingOptsPanel";
+import { GutterTheme, SplitDirection, Splitter } from "@SpComponents/Splitter";
import StanFileEditor from "@SpComponents/StanFileEditor";
-import ProjectContextProvider, {
- ProjectContext,
-} from "@SpCore/ProjectContextProvider";
+import { ProjectContext } from "@SpCore/ProjectContextProvider";
import {
modelHasUnsavedChanges,
modelHasUnsavedDataFileChanges,
ProjectKnownFiles,
SamplingOpts,
} from "@SpCore/ProjectDataModel";
-import LeftPanel from "@SpPages/LeftPanel";
+import Sidebar, { drawerWidth } from "@SpPages/Sidebar";
import TopBar from "@SpPages/TopBar";
import useStanSampler from "@SpStanSampler/useStanSampler";
import {
@@ -27,143 +30,71 @@ import {
} from "react";
type Props = {
- width: number;
- height: number;
+ //
};
-const HomePage: FunctionComponent = ({ width, height }) => {
- // NOTE: We should probably move the ProjectContextProvider up to the App or MainWindow
- // component; however this will wait on routing refactor since I don't want to add the `route`
- // item in those contexts in this PR
- return (
-
-
-
- );
-};
-
-const HomePageChild: FunctionComponent = ({ width, height }) => {
+const HomePage: FunctionComponent = () => {
const { data } = useContext(ProjectContext);
const [compiledMainJsUrl, setCompiledMainJsUrl] = useState("");
- const [leftPanelCollapsed, setLeftPanelCollapsed] = useState(
- determineShouldBeInitiallyCollapsed(width),
- );
- const expandedLeftPanelWidth = determineLeftPanelWidth(width); // what the width would be if expanded
- const leftPanelWidth = leftPanelCollapsed ? 20 : expandedLeftPanelWidth; // the actual width
+ const smallScreen = useMediaQuery("(max-width:600px)");
+
+ const [leftPanelCollapsed, setLeftPanelCollapsed] = useState(smallScreen);
// We automatically collapse the panel if user has resized the window to be
// too small but we only want to do this right when we cross the threshold,
// not every time we resize by a pixel. Similar for expanding the panel when
// we cross the threshold in the other direction.
- const lastShouldBeCollapsed = useRef(
- determineShouldBeInitiallyCollapsed(width),
- );
+ const lastShouldBeCollapsed = useRef(smallScreen);
useEffect(() => {
- const shouldBeCollapsed = determineShouldBeInitiallyCollapsed(width);
- if (shouldBeCollapsed !== lastShouldBeCollapsed.current) {
- lastShouldBeCollapsed.current = shouldBeCollapsed;
- setLeftPanelCollapsed(shouldBeCollapsed);
+ if (smallScreen !== lastShouldBeCollapsed.current) {
+ lastShouldBeCollapsed.current = smallScreen;
+ setLeftPanelCollapsed(smallScreen);
}
- }, [width]);
-
- const topBarHeight = 22;
+ }, [smallScreen]);
useEffect(() => {
document.title = "Stan Playground - Editing " + data.meta.title;
}, [data.meta.title]);
return (
-
-
-
-
-
-
-
-
+
+
+
+
+
+
-
-
+
+
-
-
+
+
);
};
-// the width of the left panel when it is expanded based on the overall width
-const determineLeftPanelWidth = (width: number) => {
- const minWidth = 150;
- const maxWidth = 250;
- return Math.min(maxWidth, Math.max(minWidth, width / 4));
-};
-
-// whether the left panel should be collapsed initially based on the overall
-// width
-const determineShouldBeInitiallyCollapsed = (width: number) => {
- return width < 800;
-};
-
type LeftViewProps = {
- width: number;
- height: number;
setCompiledMainJsUrl: (url: string) => void;
};
const LeftView: FunctionComponent = ({
- width,
- height,
setCompiledMainJsUrl,
}) => {
const { data, update } = useContext(ProjectContext);
return (
= ({
setCompiledUrl={setCompiledMainJsUrl}
/>
@@ -210,14 +139,10 @@ const LeftView: FunctionComponent = ({
};
type RightViewProps = {
- width: number;
- height: number;
compiledMainJsUrl?: string;
};
const RightView: FunctionComponent = ({
- width,
- height,
compiledMainJsUrl,
}) => {
const { data, update } = useContext(ProjectContext);
@@ -228,8 +153,6 @@ const RightView: FunctionComponent = ({
return undefined;
}
}, [data.dataFileContent]);
- const samplingOptsPanelHeight = 160;
- const samplingOptsPanelWidth = Math.min(180, width / 2);
const setSamplingOpts = useCallback(
(opts: SamplingOpts) => {
@@ -241,53 +164,51 @@ const RightView: FunctionComponent = ({
const { sampler, latestRun } = useStanSampler(compiledMainJsUrl);
const isSampling = latestRun.status === "sampling";
return (
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
);
};
+// adapted from https://mui.com/material-ui/react-drawer/#persistent-drawer
+const MovingBox = styled(Box, {
+ shouldForwardProp: (prop) => prop !== "open",
+})<{
+ open?: boolean;
+}>(({ theme, open }) => ({
+ flexGrow: 1,
+ transition: theme.transitions.create("padding", {
+ easing: theme.transitions.easing.sharp,
+ duration: theme.transitions.duration.leavingScreen,
+ }),
+ paddingLeft: `${drawerWidth}px`,
+ ...(open && {
+ transition: theme.transitions.create("padding", {
+ easing: theme.transitions.easing.easeOut,
+ duration: theme.transitions.duration.enteringScreen,
+ }),
+ paddingLeft: 0,
+ }),
+}));
+
export default HomePage;
diff --git a/gui/src/app/pages/HomePage/LoadProjectWindow.tsx b/gui/src/app/pages/HomePage/LoadProjectWindow.tsx
index 24668ce4..27706868 100644
--- a/gui/src/app/pages/HomePage/LoadProjectWindow.tsx
+++ b/gui/src/app/pages/HomePage/LoadProjectWindow.tsx
@@ -1,3 +1,4 @@
+import Button from "@mui/material/Button";
import {
FieldsContentsMap,
FileNames,
@@ -130,15 +131,15 @@ const LoadProjectWindow: FunctionComponent = ({
)}
{showReplaceProjectOptions && (
-
- importUploadedFiles({ replaceProject: false })}
>
Load into EXISTING project
-
+
)}
diff --git a/gui/src/app/pages/HomePage/SaveProjectWindow.tsx b/gui/src/app/pages/HomePage/SaveProjectWindow.tsx
index 70d6a2c1..d620d071 100644
--- a/gui/src/app/pages/HomePage/SaveProjectWindow.tsx
+++ b/gui/src/app/pages/HomePage/SaveProjectWindow.tsx
@@ -5,6 +5,7 @@ import { FileRegistry, mapModelToFileManifest } from "@SpCore/FileMapping";
import { ProjectContext } from "@SpCore/ProjectContextProvider";
import saveAsGitHubGist from "@SpCore/gists/saveAsGitHubGist";
import { triggerDownload } from "@SpUtil/triggerDownload";
+import Button from "@mui/material/Button";
type SaveProjectWindowProps = {
onClose: () => void;
@@ -45,7 +46,7 @@ const SaveProjectWindow: FunctionComponent = ({
{!exportingToGist && (
- {
serializeAsZip(data).then(([zipBlob, name]) =>
triggerDownload(zipBlob, `SP-${name}.zip`, onClose),
@@ -53,15 +54,15 @@ const SaveProjectWindow: FunctionComponent = ({
}}
>
Save to .zip file
-
+
- {
setExportingToGist(true);
}}
>
Save to GitHub Gist
-
+
)}
{exportingToGist && (
@@ -164,11 +165,11 @@ const GistExportView: FunctionComponent = ({
{!gistUrl && (
-
+
Save to GitHub Gist
-
+
- Cancel
+ Cancel
)}
{gistUrl && (
@@ -193,7 +194,7 @@ const GistExportView: FunctionComponent = ({
- Close
+ Close