diff --git a/gui/package.json b/gui/package.json index a5cd407e..20a2fcbf 100644 --- a/gui/package.json +++ b/gui/package.json @@ -20,11 +20,11 @@ "test-watch": "vitest" }, "dependencies": { - "@devbookhq/splitter": "^1.4.2", "@emotion/react": "^11.11.4", "@emotion/styled": "^11.11.5", "@fi-sci/misc": "^0.0.8", "@fi-sci/modal-window": "^0.0.2", + "@geoffcox/react-splitter": "^2.1.2", "@monaco-editor/react": "^4.6.0", "@mui/icons-material": "^5.15.17", "@mui/material": "^5.15.17", diff --git a/gui/src/app/FileEditor/StanFileEditor.tsx b/gui/src/app/FileEditor/StanFileEditor.tsx index 1ed6df8a..50c10173 100644 --- a/gui/src/app/FileEditor/StanFileEditor.tsx +++ b/gui/src/app/FileEditor/StanFileEditor.tsx @@ -1,8 +1,8 @@ +import { Split } from "@geoffcox/react-splitter"; import { AutoFixHigh, Cancel, Settings } from "@mui/icons-material"; -import useMediaQuery from "@mui/material/useMediaQuery"; -import { SplitDirection, Splitter } from "@SpComponents/Splitter"; import StanCompileResultWindow from "@SpComponents/StanCompileResultWindow"; -import TextEditor, { ToolbarItem } from "@SpComponents/TextEditor"; +import TextEditor from "@SpComponents/TextEditor"; +import { ToolbarItem } from "@SpComponents/ToolBar"; import compileStanProgram from "@SpStanc/compileStanProgram"; import { stancErrorsToCodeMarkers } from "@SpStanc/Linting"; import useStanc from "@SpStanc/useStanc"; @@ -123,7 +123,6 @@ const StanFileEditor: FunctionComponent = ({ } }, [fileContent, handleCompile, didInitialCompile]); - const showLabelsOnButtons = useMediaQuery("(min-width:600px)"); const [syntaxWindowVisible, setSyntaxWindowVisible] = useState(false); const toolbarItems: ToolbarItem[] = useMemo(() => { @@ -134,7 +133,7 @@ const StanFileEditor: FunctionComponent = ({ ret.push({ type: "button", icon: , - label: showLabelsOnButtons ? "Syntax error" : "", + label: "Syntax error", color: "darkred", tooltip: "Syntax error in Stan file", onClick: () => { @@ -145,7 +144,7 @@ const StanFileEditor: FunctionComponent = ({ ret.push({ type: "button", icon: , - label: showLabelsOnButtons ? "Syntax warning" : "", + label: "Syntax warning", color: "blue", tooltip: "Syntax warning in Stan file", onClick: () => { @@ -155,17 +154,15 @@ const StanFileEditor: FunctionComponent = ({ } // auto format - if (!readOnly) { - if (editedFileContent) { - ret.push({ - type: "button", - icon: , - tooltip: "Auto format this stan file", - label: showLabelsOnButtons ? "auto format" : undefined, - onClick: requestFormat, - color: "darkblue", - }); - } + if (!readOnly && editedFileContent && validSyntax) { + ret.push({ + type: "button", + icon: , + tooltip: "Auto format this stan file", + label: "Auto format", + onClick: requestFormat, + color: "darkblue", + }); } if (editedFileContent && editedFileContent === fileContent) { if (compileStatus !== "compiling") { @@ -173,7 +170,7 @@ const StanFileEditor: FunctionComponent = ({ ret.push({ type: "button", tooltip: "Compile Stan model", - label: "compile", + label: "Compile", icon: , onClick: handleCompile, color: "darkblue", @@ -183,7 +180,8 @@ const StanFileEditor: FunctionComponent = ({ if (compileStatus !== "") { ret.push({ type: "text", - label: compileMessage, + label: + compileMessage.charAt(0).toUpperCase() + compileMessage.slice(1), color: compileStatus === "compiled" ? "green" @@ -200,7 +198,6 @@ const StanFileEditor: FunctionComponent = ({ fileContent, handleCompile, requestFormat, - showLabelsOnButtons, validSyntax, compileStatus, compileMessage, @@ -219,25 +216,30 @@ const StanFileEditor: FunctionComponent = ({ <> ); - const initialSizes = syntaxWindowVisible ? [60, 40] : [100, 0]; + const editor = ( + + ); return ( - - + + {editor} {window} - + ); }; diff --git a/gui/src/app/FileEditor/TextEditor.tsx b/gui/src/app/FileEditor/TextEditor.tsx index 0156df67..7931e46c 100644 --- a/gui/src/app/FileEditor/TextEditor.tsx +++ b/gui/src/app/FileEditor/TextEditor.tsx @@ -1,16 +1,15 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { SmallIconButton } from "@fi-sci/misc"; import { Editor, loader, useMonaco } from "@monaco-editor/react"; -import { Save } from "@mui/icons-material"; -import Link from "@mui/material/Link"; + import monacoAddStanLang from "@SpComponents/stanLang"; +import { ToolBar, ToolbarItem } from "@SpComponents/ToolBar"; import { CodeMarker } from "@SpStanc/Linting"; import { editor, MarkerSeverity } from "monaco-editor"; import { FunctionComponent, - PropsWithChildren, useCallback, useEffect, + useMemo, useState, } from "react"; @@ -30,21 +29,6 @@ type Props = { contentOnEmpty?: string | HTMLSpanElement; }; -export type ToolbarItem = - | { - type: "button"; - tooltip?: string; - label?: string; - icon?: any; - onClick?: () => void; - color?: string; - } - | { - type: "text"; - label: string; - color?: string; - }; - const TextEditor: FunctionComponent = ({ text, onSaveText, @@ -131,32 +115,19 @@ const TextEditor: FunctionComponent = ({ [onSaveText, readOnly], ); + const edited = useMemo(() => { + return editedText !== text; + }, [editedText, text]); + return (
- -
- {label} -     - {!readOnly && text !== editedText && ( - } - title="Save file" - disabled={text === editedText} - label="save" - /> - )} -     - {editedText !== text && edited} -     - {readOnly && read only} -     - {toolbarItems && - toolbarItems.map((item, i) => ( - - ))} -
-
+ = ({ readOnly, domReadOnly: readOnly, wordWrap: "on", + minimap: { enabled: false }, }} />
@@ -186,61 +158,6 @@ const toMonacoMarkerSeverity = ( } }; -const ToolbarItemComponent: FunctionComponent<{ item: ToolbarItem }> = ({ - item, -}) => { - if (item.type === "button") { - const { onClick, color, label, tooltip, icon } = item; - if (icon) { - return ( - - -     - - ); - } else { - if (!onClick) { - return ( - - {label}    - - ); - } - return ( - - - {label} - -     - - ); - } - } else if (item.type === "text") { - return ( - - {item.label}    - - ); - } else { - return unknown toolbar item type; - } -}; - -const NotSelectable: FunctionComponent = ({ children }) => { - return
{children}
; -}; - const createHintTextContentWidget = (content: string | HTMLSpanElement) => { return { getDomNode: () => { diff --git a/gui/src/app/FileEditor/ToolBar.tsx b/gui/src/app/FileEditor/ToolBar.tsx new file mode 100644 index 00000000..51ced36b --- /dev/null +++ b/gui/src/app/FileEditor/ToolBar.tsx @@ -0,0 +1,125 @@ +import { FunctionComponent, useMemo } from "react"; +import { Save } from "@mui/icons-material"; +import Link from "@mui/material/Link"; +import IconButton from "@mui/material/IconButton"; + +export type ToolbarItem = + | { + type: "button"; + tooltip?: string; + label?: string; + icon?: any; + onClick: () => void; + color?: string; + } + | { + type: "text"; + label: string; + color?: string; + }; + +type ToolbarProps = { + items: ToolbarItem[]; + label: string; + onSaveText: () => void; + edited: boolean; + readOnly: boolean; +}; + +export const ToolBar: FunctionComponent = ({ + items, + label, + onSaveText, + edited, + readOnly, +}) => { + const toolBarItems = useMemo(() => { + const editorItems: ToolbarItem[] = []; + + if (readOnly) { + editorItems.push({ + type: "text", + label: "Read Only", + color: "gray", + }); + } else if (edited) { + editorItems.push({ + type: "button", + icon: , + onClick: onSaveText, + tooltip: "Save file", + label: "Save", + }); + editorItems.push({ + type: "text", + label: "Edited", + color: "red", + }); + } + + return editorItems.concat(items); + }, [edited, items, onSaveText, readOnly]); + + return ( +
+
+ {label} + {toolBarItems && + toolBarItems.map((item, i) => ( + + ))} +
+
+ ); +}; + +const ToolbarItemComponent: FunctionComponent<{ item: ToolbarItem }> = ({ + item, +}) => { + if (item.type === "button") { + const { onClick, color, label, tooltip, icon } = item; + if (icon) { + return ( + + + {icon} + {label && {label}} + + + ); + } else { + return ( + + + {label} + +     + + ); + } + } else if (item.type === "text") { + return ( + + {item.label}    + + ); + } else { + return unknown toolbar item type; + } +}; diff --git a/gui/src/app/Scripting/PlottingScriptEditor.tsx b/gui/src/app/Scripting/PlottingScriptEditor.tsx index 0a6028bc..64aed71c 100644 --- a/gui/src/app/Scripting/PlottingScriptEditor.tsx +++ b/gui/src/app/Scripting/PlottingScriptEditor.tsx @@ -1,15 +1,15 @@ import { FunctionComponent, RefObject } from "react"; import ScriptEditor, { ScriptEditorProps } from "./ScriptEditor"; -import { SplitDirection, Splitter } from "@SpComponents/Splitter"; +import { Split } from "@geoffcox/react-splitter"; const PlottingScriptEditor: FunctionComponent< ScriptEditorProps & { imagesRef: RefObject } > = (props) => { return ( - + - + ); }; diff --git a/gui/src/app/Scripting/ScriptEditor.tsx b/gui/src/app/Scripting/ScriptEditor.tsx index 97ec153f..86969d64 100644 --- a/gui/src/app/Scripting/ScriptEditor.tsx +++ b/gui/src/app/Scripting/ScriptEditor.tsx @@ -1,6 +1,6 @@ import { Help, PlayArrow } from "@mui/icons-material"; -import { SplitDirection, Splitter } from "@SpComponents/Splitter"; -import TextEditor, { ToolbarItem } from "@SpComponents/TextEditor"; +import TextEditor from "@SpComponents/TextEditor"; +import { ToolbarItem } from "@SpComponents/ToolBar"; import { FileNames } from "@SpCore/FileMapping"; import { ProjectContext } from "@SpCore/ProjectContextProvider"; import { ProjectKnownFiles } from "@SpCore/ProjectDataModel"; @@ -12,6 +12,7 @@ import { useMemo, } from "react"; import { InterpreterStatus } from "./InterpreterTypes"; +import { Split } from "@geoffcox/react-splitter"; const interpreterNames = { python: "pyodide", r: "webR" } as const; @@ -91,7 +92,7 @@ const ScriptEditor: FunctionComponent = ({ ]); return ( - + = ({ contentOnEmpty={contentOnEmpty} /> - + ); }; diff --git a/gui/src/app/components/Splitter.tsx b/gui/src/app/components/Splitter.tsx deleted file mode 100644 index d167b3f3..00000000 --- a/gui/src/app/components/Splitter.tsx +++ /dev/null @@ -1,62 +0,0 @@ -// @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 index 3c941e8d..2691aa9a 100644 --- a/gui/src/app/components/TabWidget.tsx +++ b/gui/src/app/components/TabWidget.tsx @@ -31,7 +31,12 @@ const TabWidget: FunctionComponent = ({ labels, children }) => { return ( - + {labels.map((label, i) => ( ))} diff --git a/gui/src/app/pages/HomePage/HomePage.tsx b/gui/src/app/pages/HomePage/HomePage.tsx index bcd116ec..75042619 100644 --- a/gui/src/app/pages/HomePage/HomePage.tsx +++ b/gui/src/app/pages/HomePage/HomePage.tsx @@ -1,7 +1,6 @@ import Box from "@mui/material/Box"; import styled from "@mui/material/styles/styled"; import useMediaQuery from "@mui/material/useMediaQuery"; -import { GutterTheme, SplitDirection, Splitter } from "@SpComponents/Splitter"; import StanFileEditor from "@SpComponents/StanFileEditor"; import { ProjectContext } from "@SpCore/ProjectContextProvider"; import { @@ -23,6 +22,7 @@ import { FileNames } from "@SpCore/FileMapping"; import DataRWindow from "@SpScripting/DataGeneration/DataRWindow"; import DataPyWindow from "@SpScripting/DataGeneration/DataPyWindow"; import TextEditor from "@SpComponents/TextEditor"; +import { Split } from "@geoffcox/react-splitter"; type Props = { // @@ -63,14 +63,10 @@ const HomePage: FunctionComponent = () => { /> - + - + ); @@ -103,10 +99,7 @@ const LeftView: FunctionComponent = ({ }) => { const { data, update } = useContext(ProjectContext); return ( - + = ({ readOnly={false} contentOnEmpty={"Enter JSON data or use the data generation tab"} /> - + ); }; diff --git a/gui/src/localStyles.css b/gui/src/localStyles.css index 1024392e..c261bd39 100644 --- a/gui/src/localStyles.css +++ b/gui/src/localStyles.css @@ -84,28 +84,57 @@ /* Text Editors ------ */ +:root { + --editor-menu-height: 30px; +} + .EditorMenuBar { background-color: lightgray; - height: 25px; + height: var(--editor-menu-height); + position: relative; + display: flex; + align-content: center; + align-items: center; + container-name: EditorMenuBar; + container-type: inline-size; + white-space: nowrap; + overflow-x: auto; + overflow-y: hidden; } .EditorWithToolbar { - height: calc(100% - 25px); + height: calc(100% - var(--editor-menu-height)); } -span.EditorTitle { - position: relative; - top: 1px; +span.EditorToolbarItem { + padding-left: 15px; +} + +.ToolbarButtonText { + text-transform: none; + font-weight: normal; + letter-spacing: normal; + display: inline-block; + white-space: normal; padding-left: 5px; + font-size: 14px; } -.EditedText { - color: #a33; - font-size: 12px; +@container EditorMenuBar (max-width: 500px) { + .ToolbarButtonText { + display: none; + width: 0; + padding: 0; + } + span.EditorToolbarItem { + padding: 0px; + } } -.ReadOnlyText { - color: gray; +span.EditorTitle { + padding-left: 5px; + padding-right: 5px; + font-weight: 500; } .NotSelectable { diff --git a/gui/yarn.lock b/gui/yarn.lock index 093d2dc5..d923faec 100644 --- a/gui/yarn.lock +++ b/gui/yarn.lock @@ -185,6 +185,13 @@ dependencies: regenerator-runtime "^0.14.0" +"@babel/runtime@^7.2.0": + version "7.25.0" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.25.0.tgz#3af9a91c1b739c569d5d80cc917280919c544ecb" + integrity sha512-7dRy4DwXwtzBrPbZflqxnvfxLF8kdZXPkhymtDeFoFqE6ldzjQFgYTtYIFARcLEYDrqfBfYcZt1WqFxRoyC9Rw== + dependencies: + regenerator-runtime "^0.14.0" + "@babel/template@^7.24.7": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.24.7.tgz#02efcee317d0609d2c07117cb70ef8fb17ab7315" @@ -288,13 +295,6 @@ style-mod "^4.1.0" w3c-keyname "^2.2.4" -"@devbookhq/splitter@^1.4.2": - version "1.4.2" - resolved "https://registry.yarnpkg.com/@devbookhq/splitter/-/splitter-1.4.2.tgz#97fb5d015ca605847511f7420cb9d59d70b0eb89" - integrity sha512-DqJXsL7WNeDn/DyCeyoeeSpFHHoYBXscYlKNd3cJQ5d1xur73MPezHpyR2OID6Kh40TZ4KAb4hYjl5nL2+5M1g== - dependencies: - react-is "^17.0.2" - "@emotion/babel-plugin@^11.11.0": version "11.11.0" resolved "https://registry.yarnpkg.com/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz#c2d872b6a7767a9d176d007f5b31f7d504bb5d6c" @@ -589,6 +589,13 @@ resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.4.tgz#1d459cee5031893a08a0e064c406ad2130cced7c" integrity sha512-dWO2pw8hhi+WrXq1YJy2yCuWoL20PddgGaqTgVe4cOS9Q6qklXCiA1tJEqX6BEwRNSCP84/afac9hd4MS+zEUA== +"@geoffcox/react-splitter@^2.1.2": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@geoffcox/react-splitter/-/react-splitter-2.1.2.tgz#cce71c0a034029714172af30df5c462b73aabf1e" + integrity sha512-+9d+VlhS/YCxqzFVqAG8+c2umDhahuR/jpykpTWtSVIcvcf42gVcyMe7tD0w+KAvZj12IJdV1dO7K1hLg15Yrg== + dependencies: + react-measure "^2.5.2" + "@humanwhocodes/config-array@^0.11.14": version "0.11.14" resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.14.tgz#d78e481a039f7566ecc9660b4ea7fe6b1fec442b" @@ -2329,6 +2336,11 @@ get-intrinsic@^1.1.3, get-intrinsic@^1.2.1, get-intrinsic@^1.2.3, get-intrinsic@ has-symbols "^1.0.3" hasown "^2.0.0" +get-node-dimensions@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/get-node-dimensions/-/get-node-dimensions-1.2.1.tgz#fb7b4bb57060fb4247dd51c9d690dfbec56b0823" + integrity sha512-2MSPMu7S1iOTL+BOa6K1S62hB2zUAYNF/lV0gSVlOaacd087lc6nR1H1r0e3B1CerTo+RceOmi1iJW+vp21xcQ== + get-stream@^8.0.1: version "8.0.1" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-8.0.1.tgz#def9dfd71742cd7754a7761ed43749a27d02eca2" @@ -3490,7 +3502,7 @@ react-is@^16.13.1, react-is@^16.7.0: resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== -react-is@^17.0.1, react-is@^17.0.2: +react-is@^17.0.1: version "17.0.2" resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== @@ -3500,6 +3512,16 @@ react-is@^18.0.0, react-is@^18.2.0: resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.3.1.tgz#e83557dc12eae63a99e003a46388b1dcbb44db7e" integrity sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg== +react-measure@^2.5.2: + version "2.5.2" + resolved "https://registry.yarnpkg.com/react-measure/-/react-measure-2.5.2.tgz#4ffc410e8b9cb836d9455a9ff18fc1f0fca67f89" + integrity sha512-M+rpbTLWJ3FD6FXvYV6YEGvQ5tMayQ3fGrZhRPHrE9bVlBYfDCLuDcgNttYfk8IqfOI03jz6cbpqMRTUclQnaA== + dependencies: + "@babel/runtime" "^7.2.0" + get-node-dimensions "^1.2.1" + prop-types "^15.6.2" + resize-observer-polyfill "^1.5.0" + react-plotly.js@^2.6.0: version "2.6.0" resolved "https://registry.yarnpkg.com/react-plotly.js/-/react-plotly.js-2.6.0.tgz#ad6b68ee64f1b5cfa142ee92c59687f9c2c09209" @@ -3602,6 +3624,11 @@ requires-port@^1.0.0: resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" integrity sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ== +resize-observer-polyfill@^1.5.0: + version "1.5.1" + resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464" + integrity sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg== + resolve-from@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6"