From 8b0726ef016a9789f2b781cc4d78b5ddcedbab55 Mon Sep 17 00:00:00 2001 From: Joeri Exelmans Date: Sat, 15 Nov 2025 13:14:57 +0100 Subject: [PATCH 1/2] better find/replace (with highlighting of all occurrences) + more consistent error visualization --- src/App/App.css | 3 +- src/App/App.tsx | 25 +++++-- src/App/BottomPanel/BottomPanel.css | 25 +++++-- src/App/BottomPanel/BottomPanel.tsx | 41 ++++++++---- src/App/BottomPanel/FindReplace.tsx | 86 +++++++++++++++++-------- src/App/Logo/Logo.tsx | 6 +- src/App/SideBar/ShowAST.tsx | 12 ++-- src/App/SideBar/SideBar.tsx | 2 +- src/App/TopPanel/TopPanel.tsx | 6 -- src/App/VisualEditor/ArrowSVG.tsx | 2 +- src/App/VisualEditor/RountangleSVG.tsx | 2 +- src/App/VisualEditor/TextSVG.tsx | 62 +++++++++++------- src/App/VisualEditor/VisualEditor.css | 55 +++++++++------- src/App/VisualEditor/VisualEditor.tsx | 15 +++-- src/App/VisualEditor/hooks/useMouse.tsx | 2 +- src/App/hooks/useSimulator.ts | 2 - src/frontend.tsx | 4 +- src/hooks/usePersistentState.ts | 1 + src/index.css | 13 +++- src/statecharts/parser.ts | 16 ++--- todo.txt | 3 + 21 files changed, 244 insertions(+), 139 deletions(-) diff --git a/src/App/App.css b/src/App/App.css index 2929afe..02789ac 100644 --- a/src/App/App.css +++ b/src/App/App.css @@ -47,7 +47,8 @@ details:has(+ details) { } .toolbar input { - height: 22px; + height: 26px; + box-sizing: border-box; } .toolbar div { vertical-align: bottom; diff --git a/src/App/App.tsx b/src/App/App.tsx index 727e22c..9e21513 100644 --- a/src/App/App.tsx +++ b/src/App/App.tsx @@ -19,6 +19,7 @@ import { emptyState } from "@/statecharts/concrete_syntax"; import { ModalOverlay } from "./Overlays/ModalOverlay"; import { FindReplace } from "./BottomPanel/FindReplace"; import { useCustomMemo } from "@/hooks/useCustomMemo"; +import { usePersistentState } from "@/hooks/usePersistentState"; export type EditHistory = { current: VisualEditorState, @@ -51,6 +52,8 @@ export function App() { const [editHistory, setEditHistory] = useState(null); const [modal, setModal] = useState(null); + const [[findText, replaceText], setFindReplaceText] = usePersistentState("findReplaceTxt", ["", ""]); + const {commitState, replaceState, onRedo, onUndo, onRotate} = useEditor(setEditHistory); const editorState = editHistory && editHistory.current; @@ -67,9 +70,17 @@ export function App() { ([prevState, prevConns], [nextState, nextConns]) => { if ((prevState === null) !== (nextState === null)) return false; if ((prevConns === null) !== (nextConns === null)) return false; + if (prevConns === null) { + return nextConns === null; + } + if (prevState === null) { + return nextState === null; + } + if (nextConns === null) return false; + if (nextState === null) return false; // the following check is much cheaper than re-rendering everything that depends on - return connectionsEqual(prevConns!, nextConns!) - && reducedConcreteSyntaxEqual(prevState!, nextState!); + return connectionsEqual(prevConns, nextConns) + && reducedConcreteSyntaxEqual(prevState, nextState); }); const ast = parsed && parsed[0]; @@ -161,7 +172,7 @@ export function App() {
{/* top-to-bottom: top bar, editor */} -
+
{/* Top bar */}
{editorState && conns && syntaxErrors && - } + }
- {appState.showFindReplace && -
- setters.setShowFindReplace(false)}/> + {editorState && appState.showFindReplace && +
+ setters.setShowFindReplace(false)}/>
} diff --git a/src/App/BottomPanel/BottomPanel.css b/src/App/BottomPanel/BottomPanel.css index 098ef48..4b58b63 100644 --- a/src/App/BottomPanel/BottomPanel.css +++ b/src/App/BottomPanel/BottomPanel.css @@ -1,12 +1,27 @@ -.errorStatus { - /* background-color: rgb(230,0,0); */ - background-color: var(--error-color); - color: var(--background-color); +.statusBar { + background-color: var(--statusbar-bg-color); + color: var(--statusbar-fg-color); +} + +.statusBar.error { + background-color: var(--statusbar-error-bg-color); + color: var(--statusbar-error-fg-color); +} + +.statusBar summary:hover { + background-color: color-mix(in srgb, var(--statusbar-bg-color) 60%, var(--background-color)); +} +.statusBar.error summary:hover { + background-color: color-mix(in srgb, var(--statusbar-error-bg-color) 70%, var(--text-color)); +} + +.statusBar a { + color: inherit; } .greeter { /* border-top: 1px var(--separator-color) solid; */ - background-color: var(--greeter-bg-color); + background-color: var(--statusbar-bg-color); } .bottom { diff --git a/src/App/BottomPanel/BottomPanel.tsx b/src/App/BottomPanel/BottomPanel.tsx index d1f5e3a..fece9bf 100644 --- a/src/App/BottomPanel/BottomPanel.tsx +++ b/src/App/BottomPanel/BottomPanel.tsx @@ -6,7 +6,6 @@ import "./BottomPanel.css"; import { PersistentDetailsLocalStorage } from "../Components/PersistentDetails"; import { Logo } from "@/App/Logo/Logo"; import { AppState } from "../App"; -import { FindReplace } from "./FindReplace"; import { VisualEditorState } from "../VisualEditor/VisualEditor"; import { Setters } from "../makePartialSetter"; @@ -16,7 +15,8 @@ export function BottomPanel(props: {errors: TraceableError[], setEditorState: Di const [greeting, setGreeting] = useState(
- Welcome to + Welcome to +
); @@ -26,23 +26,38 @@ export function BottomPanel(props: {errors: TraceableError[], setEditorState: Di }, 2000); }, []); - return
+ return
+ {greeting} {/* {props.showFindReplace &&
props.setShowFindReplace(false)}/>
} */} -
+
+
- {props.errors.length} errors -
- {props.errors.map(({message, shapeUid})=> -
- {shapeUid}: {message} -
)} -
-
+ {props.errors.length} errors +
+ {props.errors.map(({message, shapeUid})=> +
+ {shapeUid}: {message} +
)} +
+ +
+ {/*
*/} +
+ switch to  + {location.host === "localhost:3000" ? + production + : development + } +  mode +
+  |  +
- {greeting}
; } diff --git a/src/App/BottomPanel/FindReplace.tsx b/src/App/BottomPanel/FindReplace.tsx index 4ea9326..2b9b85b 100644 --- a/src/App/BottomPanel/FindReplace.tsx +++ b/src/App/BottomPanel/FindReplace.tsx @@ -1,48 +1,82 @@ -import { Dispatch, useCallback, useEffect } from "react"; +import { Dispatch, FormEvent, SetStateAction, useCallback } from "react"; import { VisualEditorState } from "../VisualEditor/VisualEditor"; -import { usePersistentState } from "@/hooks/usePersistentState"; import CloseIcon from '@mui/icons-material/Close'; -import SwapHorizIcon from '@mui/icons-material/SwapHoriz'; -import { useShortcuts } from "@/hooks/useShortcuts"; +import SwapVertIcon from '@mui/icons-material/SwapVert'; type FindReplaceProps = { + findText: string, + replaceText: string, + setFindReplaceText: Dispatch>, + cs: VisualEditorState, setCS: Dispatch<(oldState: VisualEditorState) => VisualEditorState>, - // setModal: (modal: null) => void; hide: () => void, }; -export function FindReplace({setCS, hide}: FindReplaceProps) { - const [findTxt, setFindText] = usePersistentState("findTxt", ""); - const [replaceTxt, setReplaceTxt] = usePersistentState("replaceTxt", ""); - +export function FindReplace({findText, replaceText, setFindReplaceText, cs, setCS, hide}: FindReplaceProps) { const onReplace = useCallback(() => { setCS(cs => { return { ...cs, texts: cs.texts.map(txt => ({ ...txt, - text: txt.text.replaceAll(findTxt, replaceTxt) + text: txt.text.replaceAll(findText, replaceText) })), }; }); - }, [findTxt, replaceTxt]); - - useShortcuts([ - {keys: ["Enter"], action: onReplace}, - ]) + }, [findText, replaceText, setCS]); const onSwap = useCallback(() => { - setReplaceTxt(findTxt); - setFindText(replaceTxt); - }, [findTxt, replaceTxt]); + setFindReplaceText(([findText, replaceText]) => [replaceText, findText]); + }, [findText, replaceText]); - return
- setFindText(e.target.value)} style={{width:300}}/> - - setReplaceTxt(e.target.value))} style={{width:300}}/> -   - - -
; + const onSubmit = useCallback((e: FormEvent) => { + e.preventDefault(); + e.stopPropagation(); + onReplace(); + // onSwap(); + }, [findText, replaceText, onSwap, onReplace]); + + const n = findText === "" ? 0 : cs.texts.reduce((count, txt) => count+(txt.text.indexOf(findText) !== -1 ? 1: 0), 0); + + return
+
+
+ setFindReplaceText(([_, replaceText]) => [e.target.value, replaceText])} + style={{flexGrow: 1, minWidth: 20}}/> +
+ setFindReplaceText(([findText, _]) => [findText, e.target.value]))} + style={{flexGrow: 1, minWidth: 20}}/> +
+
+
+ + +
+ +
+
+
; } \ No newline at end of file diff --git a/src/App/Logo/Logo.tsx b/src/App/Logo/Logo.tsx index 1aa429f..e86a7fa 100644 --- a/src/App/Logo/Logo.tsx +++ b/src/App/Logo/Logo.tsx @@ -1,6 +1,8 @@ +import { SVGAttributes, SVGProps } from "react"; + // i couldn't find a better way to make the text in the logo adapt to light/dark mode... -export function Logo() { - return +export function Logo(props: SVGAttributes) { + return
; }) }, (prevProps, nextProps) => { - console.log('onRaise changed:', prevProps.onRaise === nextProps.onRaise, prevProps.onRaise, nextProps.onRaise); return prevProps.onRaise === nextProps.onRaise && prevProps.disabled === nextProps.disabled && jsonDeepEqual(prevProps.inputEvents, nextProps.inputEvents); diff --git a/src/App/SideBar/SideBar.tsx b/src/App/SideBar/SideBar.tsx index a6e6048..a32e373 100644 --- a/src/App/SideBar/SideBar.tsx +++ b/src/App/SideBar/SideBar.tsx @@ -196,7 +196,7 @@ export const SideBar = memo(function SideBar({showExecutionTrace, showConnection - setProperties(properties => properties.toSpliced(i, 1, e.target.value))}/> + setProperties(properties => properties.toSpliced(i, 1, e.target.value))} placeholder='write MTL property...'/> diff --git a/src/App/TopPanel/TopPanel.tsx b/src/App/TopPanel/TopPanel.tsx index 32f8209..32ed918 100644 --- a/src/App/TopPanel/TopPanel.tsx +++ b/src/App/TopPanel/TopPanel.tsx @@ -246,11 +246,5 @@ export const TopPanel = memo(function TopPanel({trace, time, setTime, onUndo, on
-
- {location.host === "localhost:3000" ? - production - : development - } -
; }); diff --git a/src/App/VisualEditor/ArrowSVG.tsx b/src/App/VisualEditor/ArrowSVG.tsx index d09128d..6e322b3 100644 --- a/src/App/VisualEditor/ArrowSVG.tsx +++ b/src/App/VisualEditor/ArrowSVG.tsx @@ -32,7 +32,7 @@ export const ArrowSVG = memo(function(props: { arrow: Arrow; selected: ArrowPart data-parts="start end" /> {props.error && {props.rountangle.uid} {props.error && - {props.error}} + {props.error}} void, setModal: Dispatch>}) { - const commonProps = { - "data-uid": props.text.uid, - "data-parts": "text", - textAnchor: "middle" as "middle", - className: "draggableText" - + (props.selected ? " selected":"") - + (props.highlight ? " highlight":""), - style: {whiteSpace: "preserve"}, - } - - let textNode; - if (props.error?.data?.location) { - const {start,end} = props.error.data.location; - textNode = <> - {props.text.text.slice(0, start.offset)} - - {props.text.text.slice(start.offset, end.offset)} - {start.offset === end.offset && <>_} +export const FragmentedText = function FragmentedText({start, end, text, highlightClassName, uid, parts, ...rest}: {start: number, end: number, text: string, highlightClassName: string, uid: string, parts: string} & SVGTextElementAttributes) { + if (start !== -1 && start !== end) { + return + {text.slice(0, start)} + + {text.slice(start, end)} - {props.text.text.slice(end.offset)} - - {props.error.message}; + {text.slice(end)} + ; } else { - textNode = {props.text.text}; + return + {text} + } +} + +export const TextSVG = memo(function TextSVG(props: {text: Text, error: TraceableError|undefined, selected: boolean, highlight: boolean, onEdit: (text: Text, newText: string) => void, setModal: Dispatch>, findText: string}) { + + const className = "draggableText" + + (props.selected ? " selected":"") + + (props.highlight ? " highlight":"") + + (props.error ? " error":""); + + const found = props.text.text.indexOf(props.findText); + const start = (found >= 0) ? found : -1 + const end = (found >= 0) ? found + props.findText.length : -1; + + const textNode = ; return {textNode} {props.text.text} + {props.error && + {props.error.message} + } ; }, (prevProps, newProps) => { return jsonDeepEqual(prevProps.text, newProps) @@ -52,4 +65,5 @@ export const TextSVG = memo(function TextSVG(props: {text: Text, error: Traceabl && prevProps.setModal === newProps.setModal && prevProps.error === newProps.error && prevProps.selected === newProps.selected + && prevProps.findText === newProps.findText }); diff --git a/src/App/VisualEditor/VisualEditor.css b/src/App/VisualEditor/VisualEditor.css index 4153853..2679255 100644 --- a/src/App/VisualEditor/VisualEditor.css +++ b/src/App/VisualEditor/VisualEditor.css @@ -112,10 +112,6 @@ line.selected, circle.selected { stroke-width: 4px; } -.draggableText.selected, .draggableText.selected:hover { - fill: var(--accent-border-color); - font-weight: 600; -} text.helper { fill: rgba(0,0,0,0); stroke: rgba(0,0,0,0); @@ -126,24 +122,41 @@ text.helper:hover { cursor: grab; } -.draggableText, .draggableText.highlight { +.draggableText { paint-order: stroke; fill: var(--text-color); stroke: var(--background-color); stroke-width: 4px; - stroke-linecap: butt; - stroke-linejoin: miter; - stroke-opacity: 1; - fill-opacity:1; + text-anchor: middle; + white-space: preserve; } - -.draggableText.highlight:not(.selected) { - fill: var(--associated-color); - font-weight: 600; -} - .draggableText.selected { fill: var(--accent-border-color); + font-weight: 600; +} +.draggableText.highlight:not(.selected) { + fill: var(--associated-color); +} +.draggableText.error:not(.selected) { + fill: var(--error-color); +} + +text.errorHover { + display: none; + paint-order: stroke; + fill: var(--error-color); + stroke: var(--background-color); + stroke-width: 4px; + font-weight: 600; + white-space: preserve; +} +g:hover > text.errorHover { + display: inline; +} + +tspan.findText { + fill: var(--foundtext-color); + font-weight: 600; } .highlight:not(.selected):not(text) { @@ -152,7 +165,7 @@ text.helper:hover { fill: none; } -.arrow.error { +.arrow.error:not(.selected) { stroke: var(--error-color); } .arrow.fired { @@ -172,19 +185,13 @@ text.helper:hover { } } - -text.error, tspan.error { - fill: var(--error-color); - font-weight: 600; -} - -.errorHover { +/* .errorHover { display: none; } g:hover > .errorHover { display: inline; -} +} */ text.uid { fill: var(--separator-color); diff --git a/src/App/VisualEditor/VisualEditor.tsx b/src/App/VisualEditor/VisualEditor.tsx index 0a0b2d6..e11e4ca 100644 --- a/src/App/VisualEditor/VisualEditor.tsx +++ b/src/App/VisualEditor/VisualEditor.tsx @@ -52,9 +52,10 @@ type VisualEditorProps = { highlightTransitions: string[], setModal: Dispatch>, zoom: number; + findText: string; }; -export const VisualEditor = memo(function VisualEditor({state, commitState, replaceState, conns, syntaxErrors: errors, insertMode, highlightActive, highlightTransitions, setModal, zoom}: VisualEditorProps) { +export const VisualEditor = memo(function VisualEditor({state, commitState, replaceState, conns, syntaxErrors: errors, insertMode, highlightActive, highlightTransitions, setModal, zoom, findText}: VisualEditorProps) { // While dragging, the editor is in a temporary state (a state that is not committed to the edit history). If the temporary state is not null, then this state will be what you see. // const [temporaryState, setTemporaryState] = useState(null); @@ -199,8 +200,6 @@ export const VisualEditor = memo(function VisualEditor({state, commitState, repl - {(rootErrors.length>0) && {rootErrors.join(' ')}} - @@ -235,7 +234,9 @@ export const VisualEditor = memo(function VisualEditor({state, commitState, repl } )} - + + + {(rootErrors.length>0) && {rootErrors.join('\n')}} {selectionRect} ; @@ -282,7 +283,7 @@ const Diamonds = memo(function Diamonds({diamonds, selection, sidesToHighlight, && arraysEqual(p.errors, n.errors); }); -const Texts = memo(function Texts({texts, selection, textsToHighlight, errors, onEditText, setModal}: {texts: Text[], selection: Selection, textsToHighlight: {[key: string]: boolean}, errors: TraceableError[], onEditText: (text: Text, newText: string) => void, setModal: Dispatch>}) { +const Texts = memo(function Texts({texts, selection, textsToHighlight, errors, onEditText, setModal, findText}: {texts: Text[], selection: Selection, textsToHighlight: {[key: string]: boolean}, errors: TraceableError[], onEditText: (text: Text, newText: string) => void, setModal: Dispatch>, findText: string}) { return <>{texts.map(txt => { return })}; }, (p, n) => { @@ -300,6 +302,7 @@ const Texts = memo(function Texts({texts, selection, textsToHighlight, errors, o && objectsEqual(p.textsToHighlight, n.textsToHighlight) && arraysEqual(p.errors, n.errors) && p.onEditText === n.onEditText - && p.setModal === n.setModal; + && p.setModal === n.setModal + && p.findText === n.findText; }); diff --git a/src/App/VisualEditor/hooks/useMouse.tsx b/src/App/VisualEditor/hooks/useMouse.tsx index a89b08b..6c5ebcf 100644 --- a/src/App/VisualEditor/hooks/useMouse.tsx +++ b/src/App/VisualEditor/hooks/useMouse.tsx @@ -239,7 +239,7 @@ export function useMouse( ...t, topLeft: addV2D(t.topLeft, pointerDelta), } - }), + }).toSorted((a,b) => a.topLeft.y - b.topLeft.y), })); setDragging(true); } diff --git a/src/App/hooks/useSimulator.ts b/src/App/hooks/useSimulator.ts index 1bd5b9f..1e68792 100644 --- a/src/App/hooks/useSimulator.ts +++ b/src/App/hooks/useSimulator.ts @@ -138,8 +138,6 @@ export function useSimulator(ast: Statechart|null, plant: Plant { // console.log('time effect:', time, currentTraceItem); diff --git a/src/frontend.tsx b/src/frontend.tsx index 1e00a46..e95c329 100644 --- a/src/frontend.tsx +++ b/src/frontend.tsx @@ -11,9 +11,9 @@ import { App } from "./App/App"; const elem = document.getElementById("root")!; const app = ( - // + - // + ); if (import.meta.hot) { diff --git a/src/hooks/usePersistentState.ts b/src/hooks/usePersistentState.ts index d59681e..137ea12 100644 --- a/src/hooks/usePersistentState.ts +++ b/src/hooks/usePersistentState.ts @@ -3,6 +3,7 @@ import { Dispatch, SetStateAction, useCallback, useState } from "react"; // like useState, but it is persisted in localStorage // important: values must be JSON-(de-)serializable export function usePersistentState(key: string, initial: T): [T, Dispatch>] { + console.log('usePersistentState ---', key); const [state, setState] = useState(() => { const recovered = localStorage.getItem(key); let parsed; diff --git a/src/index.css b/src/index.css index 78b28ca..373097f 100644 --- a/src/index.css +++ b/src/index.css @@ -16,6 +16,12 @@ html, body { --light-accent-color: light-dark(rgba(0,0,255,0.2), rgba(78, 186, 248, 0.377)); --accent-border-color: light-dark(blue, rgb(64, 185, 255)); --accent-opaque-color: light-dark(#ccccff, #305b73); + + --statusbar-bg-color: light-dark(rgb(157, 200, 255), rgb(48, 48, 68)); + --statusbar-fg-color: light-dark(rgb(0, 0, 0), white); + --statusbar-error-bg-color: light-dark(rgb(163, 0, 0), rgb(255, 82, 82)); + --statusbar-error-fg-color: light-dark(white, black); + --separator-color: light-dark(lightgrey, rgb(58, 58, 58)); --inactive-bg-color: light-dark(#f7f7f7, rgb(29, 29, 29)); --inactive-fg-color: light-dark(grey, rgb(70, 70, 70)); @@ -40,6 +46,7 @@ html, body { --input-event-hover-bg-color: light-dark(rgb(195, 224, 176), rgb(59, 88, 40)); --input-event-active-bg-color: light-dark(rgb(176, 204, 158), rgb(77, 117, 53)); --output-event-bg-color: light-dark(rgb(230, 249, 255), rgb(28, 83, 104)); + --foundtext-color: light-dark(rgb(0, 189, 164), rgb(202, 149, 218)); background-color: var(--background-color); color: var(--text-color); @@ -50,16 +57,16 @@ input { border: 1px solid var(--separator-color); } -button { +button, input[type="submit"] { background-color: var(--button-bg-color); border: 1px var(--separator-color) solid; } -button:not(:disabled):hover { +button:not(:disabled):hover, input[type="submit"]:not(:disabled):hover { background-color: var(--light-accent-color); } -button:disabled { +button:disabled, input[type="submit"]:disabled { background-color: var(--inactive-bg-color); color: var(--inactive-fg-color); } diff --git a/src/statecharts/parser.ts b/src/statecharts/parser.ts index a3287c4..89d7c8a 100644 --- a/src/statecharts/parser.ts +++ b/src/statecharts/parser.ts @@ -203,7 +203,7 @@ export function parseStatechart(concreteSyntax: ReducedConcreteSyntax, conns: Co else if (state.initial.length === 0) { errors.push({ shapeUid: state.uid, - message: "no initial state", + message: "needs initial state", }); } } @@ -216,8 +216,8 @@ export function parseStatechart(concreteSyntax: ReducedConcreteSyntax, conns: Co // step 3: figure out labels - const textsSorted = concreteSyntax.texts.toSorted((a,b) => a.topLeft.y - b.topLeft.y); - for (const text of textsSorted) { + // ASSUMPTION: text is sorted by y-coordinate + for (const text of concreteSyntax.texts) { let parsed: ParsedText; try { parsed = cachedParseLabel(text.text); // may throw @@ -226,7 +226,7 @@ export function parseStatechart(concreteSyntax: ReducedConcreteSyntax, conns: Co if (e instanceof SyntaxError) { errors.push({ shapeUid: text.uid, - message: e.message, + message: 'parser: ' + e.message, data: e, }); parsed = { @@ -248,7 +248,7 @@ export function parseStatechart(concreteSyntax: ReducedConcreteSyntax, conns: Co // triggers if (parsed.trigger.kind === "event") { if (src.kind === "pseudo") { - errors.push({shapeUid: text.uid, message: "pseudo state outgoing transition must not have event trigger"}); + errors.push({shapeUid: text.uid, message: "cannot have trigger"}); } else { const {event} = parsed.trigger; @@ -262,7 +262,7 @@ export function parseStatechart(concreteSyntax: ReducedConcreteSyntax, conns: Co } else if (parsed.trigger.kind === "after") { if (src.kind === "pseudo") { - errors.push({shapeUid: text.uid, message: "pseudo state outgoing transition must not have after-trigger"}); + errors.push({shapeUid: text.uid, message: "cannot have trigger"}); } else { src.timers.push(parsed.trigger.durationMs); @@ -274,7 +274,7 @@ export function parseStatechart(concreteSyntax: ReducedConcreteSyntax, conns: Co } else if (parsed.trigger.kind === "triggerless") { if (src.kind !== "pseudo") { - errors.push({shapeUid: text.uid, message: "triggerless transitions only allowed on pseudo-states"}); + errors.push({shapeUid: text.uid, message: "needs trigger"}); } } } @@ -296,7 +296,7 @@ export function parseStatechart(concreteSyntax: ReducedConcreteSyntax, conns: Co else { errors.push({ shapeUid: text.uid, - message: "states can only have entry/exit triggers", + message: "must belong to transition", data: {start: {offset: 0}, end: {offset: text.text.length}}, }); } diff --git a/todo.txt b/todo.txt index e0f1cd1..9f5b2d3 100644 --- a/todo.txt +++ b/todo.txt @@ -79,6 +79,9 @@ TODO https://pub.dev/packages/ploeg_tree_layout - local variable scopes +Breaking changes (after mosis25 is finished): + - state names start with # (not with //) + Publish StateBuddy paper(s): compare CS approach to other tools, not only YAKINDU z \ No newline at end of file From 456b85bf8f39ae92a15de10a34d94424ebec7cd1 Mon Sep 17 00:00:00 2001 From: Joeri Exelmans Date: Sat, 15 Nov 2025 13:34:47 +0100 Subject: [PATCH 2/2] better (less annoying) colors + default editor state is error-free --- src/App/App.tsx | 4 ++-- src/App/BottomPanel/BottomPanel.css | 2 +- src/App/BottomPanel/BottomPanel.tsx | 11 ++++------- src/index.css | 2 +- src/statecharts/concrete_syntax.ts | 19 +++++++++++++++++-- 5 files changed, 25 insertions(+), 13 deletions(-) diff --git a/src/App/App.tsx b/src/App/App.tsx index 9e21513..8390c59 100644 --- a/src/App/App.tsx +++ b/src/App/App.tsx @@ -15,7 +15,7 @@ import { useEditor } from "./hooks/useEditor"; import { useSimulator } from "./hooks/useSimulator"; import { useUrlHashState } from "../hooks/useUrlHashState"; import { plants } from "./plants"; -import { emptyState } from "@/statecharts/concrete_syntax"; +import { initialEditorState } from "@/statecharts/concrete_syntax"; import { ModalOverlay } from "./Overlays/ModalOverlay"; import { FindReplace } from "./BottomPanel/FindReplace"; import { useCustomMemo } from "@/hooks/useCustomMemo"; @@ -89,7 +89,7 @@ export function App() { const persist = useUrlHashState( recoveredState => { if (recoveredState === null) { - setEditHistory(() => ({current: emptyState, history: [], future: []})); + setEditHistory(() => ({current: initialEditorState, history: [], future: []})); } // we support two formats // @ts-ignore diff --git a/src/App/BottomPanel/BottomPanel.css b/src/App/BottomPanel/BottomPanel.css index 4b58b63..1725d16 100644 --- a/src/App/BottomPanel/BottomPanel.css +++ b/src/App/BottomPanel/BottomPanel.css @@ -21,7 +21,7 @@ .greeter { /* border-top: 1px var(--separator-color) solid; */ - background-color: var(--statusbar-bg-color); + background-color: var(--background-color); } .bottom { diff --git a/src/App/BottomPanel/BottomPanel.tsx b/src/App/BottomPanel/BottomPanel.tsx index fece9bf..bf1258d 100644 --- a/src/App/BottomPanel/BottomPanel.tsx +++ b/src/App/BottomPanel/BottomPanel.tsx @@ -13,10 +13,10 @@ import gitRev from "@/git-rev.txt"; export function BottomPanel(props: {errors: TraceableError[], setEditorState: Dispatch<(state: VisualEditorState) => VisualEditorState>} & AppState & Setters) { const [greeting, setGreeting] = useState( -
+
setGreeting(<>)}> Welcome to - +
); @@ -45,18 +45,15 @@ export function BottomPanel(props: {errors: TraceableError[], setEditorState: Di
- {/*
*/} -
+
switch to  {location.host === "localhost:3000" ? production : development }  mode -
 |  -
- Rev: {gitRev.slice(0,8)} + Rev: {gitRev.slice(0,8)}
; diff --git a/src/index.css b/src/index.css index 373097f..6444175 100644 --- a/src/index.css +++ b/src/index.css @@ -17,7 +17,7 @@ html, body { --accent-border-color: light-dark(blue, rgb(64, 185, 255)); --accent-opaque-color: light-dark(#ccccff, #305b73); - --statusbar-bg-color: light-dark(rgb(157, 200, 255), rgb(48, 48, 68)); + --statusbar-bg-color: light-dark(rgb(225, 229, 235), rgb(48, 48, 68)); --statusbar-fg-color: light-dark(rgb(0, 0, 0), white); --statusbar-error-bg-color: light-dark(rgb(163, 0, 0), rgb(255, 82, 82)); --statusbar-error-fg-color: light-dark(white, black); diff --git a/src/statecharts/concrete_syntax.ts b/src/statecharts/concrete_syntax.ts index c74b9b9..ffd1ca6 100644 --- a/src/statecharts/concrete_syntax.ts +++ b/src/statecharts/concrete_syntax.ts @@ -40,8 +40,23 @@ export type ConcreteSyntax = { export type RectSide = "left" | "top" | "right" | "bottom"; export type ArrowPart = "start" | "end"; -export const emptyState: VisualEditorState = { - rountangles: [], texts: [], arrows: [], diamonds: [], history: [], nextID: 0, selection: [], +export const initialEditorState: VisualEditorState = { + rountangles: [{ + uid:"0", + topLeft:{x:76.25,y:122.5}, + size:{x:133.75,y:103.75}, + kind:"and" + }], + diamonds:[], + history:[], + arrows:[{ + uid:"39", + start:{x:85,y:67.5}, + end:{x:116.25,y:116.25} + }], + texts:[], + nextID: 1, + selection: [], }; // used to find which rountangle an arrow connects to (src/tgt)