better find/replace (with highlighting of all occurrences) + more consistent error visualization

This commit is contained in:
Joeri Exelmans 2025-11-15 13:14:57 +01:00
parent 64aab1a6df
commit 8b0726ef01
21 changed files with 244 additions and 139 deletions

View file

@ -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 {

View file

@ -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(
<div className="greeter" style={{textAlign:'center'}}>
<span style={{fontSize: 18, fontStyle: 'italic'}}>
Welcome to <Logo/>
Welcome to
<Logo width={300} height="auto" style={{verticalAlign: 'middle'}}/>
</span>
</div>);
@ -26,23 +26,38 @@ export function BottomPanel(props: {errors: TraceableError[], setEditorState: Di
}, 2000);
}, []);
return <div className="toolbar bottom">
return <div className="bottom">
{greeting}
{/* {props.showFindReplace &&
<div>
<FindReplace setCS={props.setEditorState} hide={() => props.setShowFindReplace(false)}/>
</div>
} */}
<div className={"statusBar" + props.errors.length ? " error" : ""}>
<div className={"stackHorizontal statusBar" + (props.errors.length ? " error" : "")}>
<div style={{flexGrow:1}}>
<PersistentDetailsLocalStorage initiallyOpen={false} localStorageKey="errorsExpanded">
<summary>{props.errors.length} errors</summary>
<div style={{maxHeight: '25vh', overflow: 'auto'}}>
{props.errors.map(({message, shapeUid})=>
<div>
{shapeUid}: {message}
</div>)}
</div>
</PersistentDetailsLocalStorage>
<summary>{props.errors.length} errors</summary>
<div style={{maxHeight: '20vh', overflow: 'auto'}}>
{props.errors.map(({message, shapeUid})=>
<div>
{shapeUid}: {message}
</div>)}
</div>
</PersistentDetailsLocalStorage>
</div>
{/* <div ></div> */}
<div>
switch to&nbsp;
{location.host === "localhost:3000" ?
<a href={`https://deemz.org/public/statebuddy/${location.hash}`}>production</a>
: <a href={`http://localhost:3000/${location.hash}`}>development</a>
}
&nbsp;mode
</div>
&nbsp;|&nbsp;
<div>
Rev: <a title={"git"} href={`https://deemz.org/git/research/statebuddy/commit/${gitRev}`}>{gitRev.slice(0,8)}</a>
</div>
</div>
{greeting}
</div>;
}

View file

@ -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<SetStateAction<[string, string]>>,
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 <div className="toolbar toolbarGroup" style={{display: 'flex'}}>
<input placeholder="find" value={findTxt} onChange={e => setFindText(e.target.value)} style={{width:300}}/>
<button tabIndex={-1} onClick={onSwap}><SwapHorizIcon fontSize="small"/></button>
<input tabIndex={0} placeholder="replace" value={replaceTxt} onChange={(e => setReplaceTxt(e.target.value))} style={{width:300}}/>
&nbsp;
<button onClick={onReplace}>replace all</button>
<button onClick={hide} style={{marginLeft: 'auto'}}><CloseIcon fontSize="small"/></button>
</div>;
const onSubmit = useCallback((e: FormEvent<HTMLFormElement>) => {
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 <form onSubmit={onSubmit}>
<div className="toolbar toolbarGroup" style={{display: 'flex', flexDirection: 'row'}}>
<div style={{flexGrow:1, display: 'flex', flexDirection: 'column'}}>
<input placeholder="find"
title="old text"
value={findText}
onChange={e => setFindReplaceText(([_, replaceText]) => [e.target.value, replaceText])}
style={{flexGrow: 1, minWidth: 20}}/>
<br/>
<input tabIndex={0} placeholder="replace"
title="new text"
value={replaceText}
onChange={(e => setFindReplaceText(([findText, _]) => [findText, e.target.value]))}
style={{flexGrow: 1, minWidth: 20}}/>
</div>
<div style={{flex: '0 0 content', display: 'flex', justifyItems: 'flex-start', flexDirection: 'column'}}>
<div style={{display: 'flex'}}>
<button
type="button" // <-- prevent form submission on click
title="swap find/replace fields"
onClick={onSwap}
style={{flexGrow: 1}}>
<SwapVertIcon fontSize="small"/>
</button>
<button
type="button" // <-- prevent form submission on click
title="hide find & replace"
onClick={hide}
style={{flexGrow: 1 }}>
<CloseIcon fontSize="small"/>
</button>
</div>
<input type="submit"
disabled={n===0}
title="replace all occurrences in model"
value={`replace all (${n})`}
style={{height: 26}}/>
</div>
</div>
</form>;
}