migrated the frontend file generation, changed folder structure
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,3 +1,5 @@
|
|||||||
|
node_modules
|
||||||
|
package-lock.json
|
||||||
# postman
|
# postman
|
||||||
.postman
|
.postman
|
||||||
postman
|
postman
|
||||||
|
|||||||
26
frontend-plugin/index.html
Normal file
26
frontend-plugin/index.html
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<!-- index.html -->
|
||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
background: none;
|
||||||
|
background-color: var(--mantine-color-secondary);
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
font-family:
|
||||||
|
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
|
||||||
|
sans-serif;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/index.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
39
frontend-plugin/package.json
Normal file
39
frontend-plugin/package.json
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
{
|
||||||
|
"name": "task-plugins",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"description": "ES module task plugin",
|
||||||
|
"module": "./dist/plugin.js",
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"import": "./dist/plugin.js",
|
||||||
|
"types": "./dist/index.d.ts"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "vite build",
|
||||||
|
"build:watch": "vite build --watch",
|
||||||
|
"dev": "vite build --watch",
|
||||||
|
"type-check": "tsc --noEmit"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/react": "^18.2.0",
|
||||||
|
"@vitejs/plugin-react": "^6.0.1",
|
||||||
|
"esbuild": "^0.28.0",
|
||||||
|
"typescript": "^5.0.0",
|
||||||
|
"vite": "^8.0.10",
|
||||||
|
"vite-plugin-singlefile": "^2.3.3"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist"
|
||||||
|
],
|
||||||
|
"sideEffects": false,
|
||||||
|
"dependencies": {
|
||||||
|
"@mantine/core": "^9.1.1",
|
||||||
|
"@mantine/notifications": "^9.1.1",
|
||||||
|
"@tabler/icons-react": "^3.41.1",
|
||||||
|
"miew-react": "^0.11.0",
|
||||||
|
"react-resizable-panels": "^4.10.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
85
frontend-plugin/src/EditorPage/CodeTextArea/CodeTextArea.tsx
Normal file
85
frontend-plugin/src/EditorPage/CodeTextArea/CodeTextArea.tsx
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
import { Textarea } from "@mantine/core";
|
||||||
|
import { useEffect, useRef } from "react";
|
||||||
|
|
||||||
|
interface TextAreaProps {
|
||||||
|
text: string;
|
||||||
|
onTextChange: (text: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CodeTextArea(props: TextAreaProps) {
|
||||||
|
const inputRef = useRef(null);
|
||||||
|
const lineRef = useRef(null);
|
||||||
|
|
||||||
|
//Synchronize scrolling between the
|
||||||
|
useEffect(() => {
|
||||||
|
const inputEl = document.querySelector(".MoleculeEditInput");
|
||||||
|
const lineEl = document.querySelector(".MoleculeEditLineNumber");
|
||||||
|
if (!inputEl || !lineEl) return;
|
||||||
|
|
||||||
|
let isSyncing = false; // prevents circular scroll events
|
||||||
|
let activeEl = null; // element currently being scrolled
|
||||||
|
|
||||||
|
const syncScroll = (source: Element, target: Element) => {
|
||||||
|
if (isSyncing) return;
|
||||||
|
isSyncing = true;
|
||||||
|
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
target.scrollTop = source.scrollTop;
|
||||||
|
isSyncing = false;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onScroll = (e: Event) => {
|
||||||
|
activeEl = e.target;
|
||||||
|
if (activeEl === inputEl) {
|
||||||
|
syncScroll(inputEl, lineEl);
|
||||||
|
} else {
|
||||||
|
syncScroll(lineEl, inputEl);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
inputEl.addEventListener("scroll", onScroll, { passive: true });
|
||||||
|
lineEl.addEventListener("scroll", onScroll, { passive: true });
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
inputEl.removeEventListener("scroll", onScroll);
|
||||||
|
lineEl.removeEventListener("scroll", onScroll);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Textarea
|
||||||
|
className="MoleculeEdit"
|
||||||
|
value={props.text}
|
||||||
|
ref={inputRef}
|
||||||
|
onChange={(e) => {
|
||||||
|
props.onTextChange(e.currentTarget.value);
|
||||||
|
}}
|
||||||
|
autoFocus
|
||||||
|
wrap="no-wrap"
|
||||||
|
leftSection={
|
||||||
|
<div className="MoleculeEditLineNumber" ref={lineRef}>
|
||||||
|
{Array.from({ length: props.text.split("\n").length }, (_, i) => {
|
||||||
|
if (i > 1) {
|
||||||
|
return i - 1;
|
||||||
|
} else {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}).join("\n")}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
classNames={{
|
||||||
|
input: "MoleculeEditInput",
|
||||||
|
wrapper: "MoleculeEditWrapper",
|
||||||
|
}}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
// Stop arrow keys from reaching react-resizable-panels
|
||||||
|
if (
|
||||||
|
["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"].includes(e.key)
|
||||||
|
) {
|
||||||
|
e.stopPropagation();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
></Textarea>
|
||||||
|
);
|
||||||
|
}
|
||||||
94
frontend-plugin/src/EditorPage/EditorPage.tsx
Normal file
94
frontend-plugin/src/EditorPage/EditorPage.tsx
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Center,
|
||||||
|
Divider,
|
||||||
|
Tabs,
|
||||||
|
Text,
|
||||||
|
Textarea,
|
||||||
|
useMantineTheme,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import { Group, Panel, Separator } from "react-resizable-panels";
|
||||||
|
import "./MoleculePage.css";
|
||||||
|
import {
|
||||||
|
IconCheck,
|
||||||
|
IconCode,
|
||||||
|
IconGripVertical,
|
||||||
|
IconList,
|
||||||
|
IconX,
|
||||||
|
} from "@tabler/icons-react";
|
||||||
|
import { CodeTextArea } from "./CodeTextArea/CodeTextArea";
|
||||||
|
import MoleculeViewer from "./MoleculeViewer/MoleculeViewer";
|
||||||
|
import { MoleculeData, SelectedAtom, TaskEditorProps } from "../Types/plugin";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
function MoleculeEditorPage(props: TaskEditorProps<MoleculeData>) {
|
||||||
|
const [text, setText] = useState(props.data?.data.text);
|
||||||
|
const [selectedAtom, setSelectedAtom] = useState<SelectedAtom | null>(null);
|
||||||
|
const theme = useMantineTheme();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div style={{ display: "flex", flexDirection: "column", height: "90vh" }}>
|
||||||
|
<Divider style={{ margin: "10px" }} />
|
||||||
|
<Group style={{ flex: 1, gap: "5px" }}>
|
||||||
|
<Panel minSize={200} defaultSize={500}>
|
||||||
|
<Tabs
|
||||||
|
variant="outline"
|
||||||
|
style={{
|
||||||
|
height: "100%",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
}}
|
||||||
|
radius="lg"
|
||||||
|
defaultValue="code"
|
||||||
|
classNames={{ panel: "tabPanel" }}
|
||||||
|
>
|
||||||
|
<Tabs.List>
|
||||||
|
<Tabs.Tab value="code" leftSection={<IconCode size={12} />}>
|
||||||
|
Код
|
||||||
|
</Tabs.Tab>
|
||||||
|
<Tabs.Tab value="list" leftSection={<IconList size={12} />}>
|
||||||
|
Список
|
||||||
|
</Tabs.Tab>
|
||||||
|
</Tabs.List>
|
||||||
|
|
||||||
|
<Tabs.Panel value="code">
|
||||||
|
<CodeTextArea
|
||||||
|
text={text || ""}
|
||||||
|
onTextChange={(text) => {
|
||||||
|
setText(text);
|
||||||
|
props.setData({ text: text });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Tabs.Panel>
|
||||||
|
|
||||||
|
<Tabs.Panel value="list">
|
||||||
|
<></>
|
||||||
|
</Tabs.Panel>
|
||||||
|
</Tabs>
|
||||||
|
</Panel>
|
||||||
|
<Separator>
|
||||||
|
<Center
|
||||||
|
style={{
|
||||||
|
height: "100%",
|
||||||
|
background: theme.colors.dark[4],
|
||||||
|
borderRadius: "5px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconGripVertical size={12} />
|
||||||
|
</Center>
|
||||||
|
</Separator>
|
||||||
|
<Panel style={{ flexShrink: 0 }} minSize={200}>
|
||||||
|
<MoleculeViewer
|
||||||
|
moleculeData={props.data ? props.data.data.text : ""}
|
||||||
|
selectedAtom={selectedAtom}
|
||||||
|
setSelectedAtom={setSelectedAtom}
|
||||||
|
/>
|
||||||
|
</Panel>
|
||||||
|
</Group>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MoleculeEditorPage;
|
||||||
77
frontend-plugin/src/EditorPage/MoleculePage.css
Normal file
77
frontend-plugin/src/EditorPage/MoleculePage.css
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
.experimentButtons {
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: right;
|
||||||
|
width: fit-content;
|
||||||
|
margin-left: auto;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
text-wrap: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.MoleculeEdit {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
.MoleculeEditWrapper {
|
||||||
|
display: flex !important;
|
||||||
|
flex: 1 !important;
|
||||||
|
min-width: 0;
|
||||||
|
min-height: 0;
|
||||||
|
background-color: var(--mantine-color-secondary-filled);
|
||||||
|
}
|
||||||
|
|
||||||
|
.MoleculeEditLineNumber {
|
||||||
|
height: 100%;
|
||||||
|
padding-top: calc(1px + var(--input-padding-y, 0rem));
|
||||||
|
padding-bottom: calc(1px + var(--input-padding-y, 0rem));
|
||||||
|
font-family:
|
||||||
|
"Fira Code", "Courier New", Courier, monospace; /* Monospace fonts */
|
||||||
|
font-size: 14px; /* Comfortable size */
|
||||||
|
line-height: 1.5; /* Spacing like an editor */
|
||||||
|
letter-spacing: 0; /* Keeps punctuation aligned */
|
||||||
|
white-space: pre-line;
|
||||||
|
overflow: scroll;
|
||||||
|
scrollbar-width: none;
|
||||||
|
overscroll-behavior: none;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.MoleculeEditInput {
|
||||||
|
font-family:
|
||||||
|
"Fira Code", "Courier New", Courier, monospace; /* Monospace fonts */
|
||||||
|
font-size: 14px; /* Comfortable size */
|
||||||
|
line-height: 1.5; /* Spacing like an editor */
|
||||||
|
letter-spacing: 0; /* Keeps punctuation aligned */
|
||||||
|
white-space: pre; /* preserves spaces & tabs */
|
||||||
|
overflow-x: auto; /* horizontal scroll when needed */
|
||||||
|
overflow-y: auto; /* vertical scroll when needed */
|
||||||
|
word-wrap: normal; /* prevent wrapping */
|
||||||
|
overscroll-behavior: none;
|
||||||
|
background-color: var(--mantine-color-secondary-filled);
|
||||||
|
border-radius: 0px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Separator {
|
||||||
|
width: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.moleculeInput {
|
||||||
|
height: 250px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.moleculeInputWrapper {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.moleculeInputWrapper > * {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabPanel {
|
||||||
|
flex: 1 !important;
|
||||||
|
padding: 10px;
|
||||||
|
border-left: 1px solid var(--tab-border-color);
|
||||||
|
border-right: 1px solid var(--tab-border-color);
|
||||||
|
border-bottom: 1px solid var(--tab-border-color);
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
.moleculeViewer {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
overflow: auto;
|
||||||
|
position: relative;
|
||||||
|
overscroll-behavior: contain;
|
||||||
|
touch-action: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.atomEditor {
|
||||||
|
position: absolute;
|
||||||
|
border-radius: md;
|
||||||
|
right: 10px;
|
||||||
|
top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.atomEditor * {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
269
frontend-plugin/src/EditorPage/MoleculeViewer/MoleculeViewer.tsx
Normal file
269
frontend-plugin/src/EditorPage/MoleculeViewer/MoleculeViewer.tsx
Normal file
@@ -0,0 +1,269 @@
|
|||||||
|
import Viewer from "miew-react";
|
||||||
|
import Miew from "miew";
|
||||||
|
import {
|
||||||
|
Paper,
|
||||||
|
UnstyledButton,
|
||||||
|
useMantineColorScheme,
|
||||||
|
useMantineTheme,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import { useEffect, useMemo, useRef, useState } from "react";
|
||||||
|
import "./MoleculeViewer.css";
|
||||||
|
import { notifications } from "@mantine/notifications";
|
||||||
|
import {
|
||||||
|
IconAlertHexagon,
|
||||||
|
IconArrowBarBoth,
|
||||||
|
IconRefresh,
|
||||||
|
} from "@tabler/icons-react";
|
||||||
|
import MoleculeViewerMenu from "./MoleculeViewerMenu";
|
||||||
|
import { SelectedAtom } from "../../Types/plugin";
|
||||||
|
|
||||||
|
const callbackOnResizeFinish = (
|
||||||
|
dom_elem: HTMLElement,
|
||||||
|
beginning_callback: () => void,
|
||||||
|
end_callback: () => void,
|
||||||
|
delay = 100, // ms after resize "finishes"
|
||||||
|
) => {
|
||||||
|
let timeoutId: number | undefined;
|
||||||
|
|
||||||
|
const resizeObserver = new ResizeObserver(() => {
|
||||||
|
beginning_callback();
|
||||||
|
if (timeoutId) {
|
||||||
|
window.clearTimeout(timeoutId);
|
||||||
|
}
|
||||||
|
|
||||||
|
timeoutId = window.setTimeout(() => {
|
||||||
|
end_callback();
|
||||||
|
}, delay);
|
||||||
|
});
|
||||||
|
|
||||||
|
resizeObserver.observe(dom_elem);
|
||||||
|
|
||||||
|
return () => resizeObserver.disconnect(); // cleanup helper
|
||||||
|
};
|
||||||
|
|
||||||
|
interface MoleculeViewerProps {
|
||||||
|
moleculeData: string;
|
||||||
|
selectedAtom: SelectedAtom | null;
|
||||||
|
setSelectedAtom: (selectedAtom: SelectedAtom | null) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function MoleculeViewer(props: MoleculeViewerProps) {
|
||||||
|
const theme = useMantineTheme();
|
||||||
|
const { colorScheme } = useMantineColorScheme();
|
||||||
|
// объект загруженного редактора молекул для редактирования
|
||||||
|
const [miew, setMiew] = useState<Miew | null>(null);
|
||||||
|
const molViewerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const [isResizing, setIsResizing] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const [viewingData, setViewingData] = useState<string>(props.moleculeData);
|
||||||
|
|
||||||
|
//при загрузке сохраняем объект miew
|
||||||
|
const onInitMiew = (miew: Miew) => {
|
||||||
|
setMiew(miew);
|
||||||
|
if (miew && viewingData) {
|
||||||
|
//прогружаем молекулу
|
||||||
|
miew
|
||||||
|
.load(viewingData, {
|
||||||
|
sourceType: "immediate",
|
||||||
|
fileType: "xyz",
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
if (props.selectedAtom) {
|
||||||
|
props.setSelectedAtom(null);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
if (error.message == "Operation cancelled") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
notifications.show({
|
||||||
|
color: "orange",
|
||||||
|
radius: "md",
|
||||||
|
title: "Ошибка при отображении молекулы",
|
||||||
|
message:
|
||||||
|
"Проверте правильность написания кода молекулы, в нем содержатся ошибки",
|
||||||
|
icon: <IconAlertHexagon />,
|
||||||
|
style: { paddingLeft: "5px" },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
miew.setOptions({
|
||||||
|
settings: {
|
||||||
|
bg: {
|
||||||
|
color: theme.colors.secondaryDark[7],
|
||||||
|
},
|
||||||
|
fogAlpha: 0.7,
|
||||||
|
axes: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
//мемоизируем объект чтобы он не перегружаля при изменении [miew] состояния
|
||||||
|
const MemoViewer = useMemo(
|
||||||
|
() => <Viewer onInit={onInitMiew} />,
|
||||||
|
[viewingData],
|
||||||
|
);
|
||||||
|
|
||||||
|
//Замена станартного обработчика нажатия на молекулы
|
||||||
|
useEffect(() => {
|
||||||
|
if (molViewerRef.current && miew) {
|
||||||
|
//INFO: при изменении размера окна обносить webgl
|
||||||
|
const cleanup = callbackOnResizeFinish(
|
||||||
|
molViewerRef.current,
|
||||||
|
handleResizeStart,
|
||||||
|
handleResizeEnd,
|
||||||
|
);
|
||||||
|
//@ts-expect-error Miew class not implementing typescript correctly
|
||||||
|
miew.removeEventListener("newpick");
|
||||||
|
//@ts-expect-error Miew class not implementing typescript correctly
|
||||||
|
miew.addEventListener("newpick", handleClick);
|
||||||
|
return () => {
|
||||||
|
cleanup();
|
||||||
|
//@ts-expect-error Miew class not implementing typescript correctly
|
||||||
|
miew.removeEventListener("newpick");
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}, [molViewerRef, miew]);
|
||||||
|
|
||||||
|
//On selected Atom change, highlight it
|
||||||
|
useEffect(() => {
|
||||||
|
if (miew) {
|
||||||
|
if (!props.selectedAtom) {
|
||||||
|
//@ts-expect-error Miew class not implementing typescript correctly
|
||||||
|
miew.select("");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
//@ts-expect-error Miew class not implementing typescript correctly
|
||||||
|
miew.select("serial " + props.selectedAtom.serial, false);
|
||||||
|
//спрятать информацию встроенную
|
||||||
|
//@ts-expect-error Miew class not implementing typescript correctly
|
||||||
|
miew._msgAtomInfo.style.opacity = 0.0;
|
||||||
|
//@ts-expect-error Miew class not implementing typescript correctly
|
||||||
|
miew._msgAtomInfo.style.height = "0px";
|
||||||
|
//@ts-expect-error Miew class not implementing typescript correctly
|
||||||
|
miew._msgAtomInfo.style.overflow = "hidden";
|
||||||
|
}
|
||||||
|
}, [props.selectedAtom, miew]);
|
||||||
|
|
||||||
|
//при смене темы обновляем фон MoleculeViewer
|
||||||
|
useEffect(() => {
|
||||||
|
if (miew) {
|
||||||
|
miew.setOptions({
|
||||||
|
settings: {
|
||||||
|
bg: {
|
||||||
|
color: theme.colors.secondaryDark[7],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [colorScheme]);
|
||||||
|
|
||||||
|
//при нажятии на атом выбираем его
|
||||||
|
const handleClick = (pick: { type: string; obj: { atom: SelectedAtom } }) => {
|
||||||
|
if (miew) {
|
||||||
|
if (pick.obj.atom) {
|
||||||
|
props.setSelectedAtom(pick.obj.atom);
|
||||||
|
} else {
|
||||||
|
props.setSelectedAtom(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleResizeStart = () => {
|
||||||
|
setIsResizing(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleResizeEnd = () => {
|
||||||
|
//@ts-expect-error Miew class not implementing typescript correctly
|
||||||
|
miew._onResize();
|
||||||
|
//@ts-expect-error Miew class not implementing typescript correctly
|
||||||
|
miew._picker.handleResize();
|
||||||
|
setTimeout(() => {
|
||||||
|
setIsResizing(false);
|
||||||
|
}, 50);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseEnter = () => {
|
||||||
|
document.body.style.overflow = "hidden"; // disable page scroll
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseLeave = () => {
|
||||||
|
document.body.style.overflow = "auto"; // re-enable scroll
|
||||||
|
};
|
||||||
|
|
||||||
|
const refreshDisplay = () => {
|
||||||
|
if (props.moleculeData != viewingData) {
|
||||||
|
setViewingData(props.moleculeData);
|
||||||
|
} else {
|
||||||
|
if (miew) {
|
||||||
|
//@ts-expect-error Miew class not implementing typescript correctly
|
||||||
|
miew.resetView();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={molViewerRef} className="moleculeViewer">
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
backgroundColor: theme.colors.secondaryDark[7],
|
||||||
|
borderRadius: "5px",
|
||||||
|
overflow: "hidden",
|
||||||
|
}}
|
||||||
|
onMouseEnter={handleMouseEnter}
|
||||||
|
onMouseLeave={handleMouseLeave}
|
||||||
|
>
|
||||||
|
{MemoViewer}
|
||||||
|
{isResizing && viewingData && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
inset: 0,
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
backgroundColor: theme.colors.secondaryDark[7], // optional dim
|
||||||
|
opacity: 0.5,
|
||||||
|
zIndex: 10,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconArrowBarBoth size={80} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{props.selectedAtom && (
|
||||||
|
<Paper className="atomEditor" radius="md" p="md">
|
||||||
|
<MoleculeViewerMenu selectedAtom={props.selectedAtom} miew={miew} />
|
||||||
|
</Paper>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
left: "10px",
|
||||||
|
top: "10px",
|
||||||
|
display: "flex",
|
||||||
|
zIndex: 5,
|
||||||
|
width: "auto",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<UnstyledButton onClick={refreshDisplay}>
|
||||||
|
<IconRefresh color="white" size={25} />
|
||||||
|
</UnstyledButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MoleculeViewer;
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { Text, Divider } from "@mantine/core";
|
||||||
|
import Miew from "miew";
|
||||||
|
import "./MoleculeViewer.css";
|
||||||
|
import type { SelectedAtom } from "Types/Experiment/MoleculeEdit/MoleculeEdit";
|
||||||
|
|
||||||
|
interface MoleculeViewerMenuProps {
|
||||||
|
selectedAtom: SelectedAtom;
|
||||||
|
miew: Miew | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function MoleculeViewerMenu(props: MoleculeViewerMenuProps) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="moleculeViewerMenu">
|
||||||
|
<>
|
||||||
|
<Text ta="center">
|
||||||
|
Атом: {props.selectedAtom.name} ({props.selectedAtom.serial})
|
||||||
|
</Text>
|
||||||
|
<Divider my="sm" />
|
||||||
|
<Text ta="center">Положение X: {props.selectedAtom.position.x}</Text>
|
||||||
|
<Text ta="center">Положение Y: {props.selectedAtom.position.y}</Text>
|
||||||
|
<Text ta="center">Положение Z: {props.selectedAtom.position.z}</Text>
|
||||||
|
</>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MoleculeViewerMenu;
|
||||||
23
frontend-plugin/src/ListItem/ListItem.tsx
Normal file
23
frontend-plugin/src/ListItem/ListItem.tsx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
// ListItem/ListItem.tsx
|
||||||
|
import React from "react";
|
||||||
|
import { MoleculeData, TaskData } from "../Types/plugin";
|
||||||
|
|
||||||
|
const ListItem: React.FunctionComponent<{
|
||||||
|
data: TaskData<MoleculeData> | undefined;
|
||||||
|
}> = (props) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{ padding: "1rem", border: "1px solid #ccc", borderRadius: "4px" }}
|
||||||
|
>
|
||||||
|
<h3 style={{ margin: "0 0 0.5rem 0" }}>{props.data?.name}</h3>
|
||||||
|
<p style={{ margin: "0 0 0.5rem 0", color: "#666" }}>
|
||||||
|
{props.data?.description}
|
||||||
|
</p>
|
||||||
|
<div style={{ fontSize: "0.9rem", color: "#999" }}>
|
||||||
|
Value: {props.data?.data.text || "Not set"}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ListItem;
|
||||||
28
frontend-plugin/src/Types/plugin.tsx
Normal file
28
frontend-plugin/src/Types/plugin.tsx
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
// Types
|
||||||
|
export interface TaskData<TData = any> {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
data: TData;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TaskEditorProps<TData = any> {
|
||||||
|
data: TaskData<TData> | undefined;
|
||||||
|
setData: (data: TData) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TaskTypePlugin<TData = any> {
|
||||||
|
type: string;
|
||||||
|
ListItem: React.ComponentType<TaskData<TData> | undefined>;
|
||||||
|
Editor: React.ComponentType<TaskEditorProps<TData>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MoleculeData {
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SelectedAtom {
|
||||||
|
serial: number;
|
||||||
|
name: string;
|
||||||
|
position: { x: number; y: number; z: number };
|
||||||
|
}
|
||||||
29
frontend-plugin/src/index.css
Normal file
29
frontend-plugin/src/index.css
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
:root {
|
||||||
|
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||||
|
line-height: 1.5;
|
||||||
|
font-weight: 400;
|
||||||
|
|
||||||
|
color-scheme: light dark;
|
||||||
|
color: rgba(255, 255, 255, 0.87);
|
||||||
|
|
||||||
|
font-synthesis: none;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 4px;
|
||||||
|
height: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "Quicking"; /* Define a name for your font */
|
||||||
|
src: url("/Quicking.otf") format("opentype"); /* Specify the font file and format */
|
||||||
|
font-weight: normal; /* Optional: Define the weight of this font variant */
|
||||||
|
font-style: normal; /* Optional: Define the style of this font variant */
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--mantine-color-body: var(--mantine-color-primary-filled) !important;
|
||||||
|
}
|
||||||
22
frontend-plugin/src/index.tsx
Normal file
22
frontend-plugin/src/index.tsx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
// src/main.tsx (entry point for iframe, not index.tsx)
|
||||||
|
import ReactDOM from "react-dom/client";
|
||||||
|
import "./index.css";
|
||||||
|
import "@mantine/core/styles.css";
|
||||||
|
import Wrapper from "./wrapper";
|
||||||
|
// Parse URL parameters
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
const mode = urlParams.get("mode") == "List" ? "List" : "Editor";
|
||||||
|
|
||||||
|
// Render directly
|
||||||
|
const rootElement = document.getElementById("root");
|
||||||
|
|
||||||
|
if (rootElement) {
|
||||||
|
const root = ReactDOM.createRoot(rootElement);
|
||||||
|
|
||||||
|
root.render(<Wrapper mode={mode} />);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notify parent that plugin is ready
|
||||||
|
if (window.parent !== window) {
|
||||||
|
window.parent.postMessage({ type: "plugin-ready" }, "*");
|
||||||
|
}
|
||||||
109
frontend-plugin/src/wrapper.tsx
Normal file
109
frontend-plugin/src/wrapper.tsx
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
// src/main.tsx (entry point for iframe, not index.tsx)
|
||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import ListItem from "./ListItem/ListItem";
|
||||||
|
import MoleculeEditorPage from "./EditorPage/EditorPage";
|
||||||
|
import {
|
||||||
|
colorsTuple,
|
||||||
|
createTheme,
|
||||||
|
localStorageColorSchemeManager,
|
||||||
|
MantineProvider,
|
||||||
|
virtualColor,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import "./index.css";
|
||||||
|
import "@mantine/core/styles.css";
|
||||||
|
import { MoleculeData, TaskData } from "./Types/plugin";
|
||||||
|
|
||||||
|
interface WrapperProps {
|
||||||
|
mode: "List" | "Editor";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create setData function that communicates with parent
|
||||||
|
const setData = (newData: MoleculeData) => {
|
||||||
|
window.parent.postMessage(
|
||||||
|
{
|
||||||
|
type: "plugin-update",
|
||||||
|
data: newData,
|
||||||
|
},
|
||||||
|
"*",
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const colorSchemeManager = localStorageColorSchemeManager({
|
||||||
|
key: "my-app-color-scheme",
|
||||||
|
});
|
||||||
|
const theme = createTheme({
|
||||||
|
colors: {
|
||||||
|
primaryDark: colorsTuple("#212529"),
|
||||||
|
primaryLight: colorsTuple("#f1f3f5"),
|
||||||
|
secondaryDark: colorsTuple("#484d53"),
|
||||||
|
secondaryLight: colorsTuple("#b9bec4"),
|
||||||
|
contrastDark: colorsTuple("#f1f3f5"),
|
||||||
|
contrastLight: colorsTuple("#212529"),
|
||||||
|
secondaryContrastDark: colorsTuple("#b9bec4"),
|
||||||
|
secondaryContrastLight: colorsTuple("#484d53"),
|
||||||
|
primary: virtualColor({
|
||||||
|
name: "primary",
|
||||||
|
dark: "primaryDark",
|
||||||
|
light: "primaryLight",
|
||||||
|
}),
|
||||||
|
secondary: virtualColor({
|
||||||
|
name: "secondary",
|
||||||
|
dark: "secondaryDark",
|
||||||
|
light: "secondaryLight",
|
||||||
|
}),
|
||||||
|
contrast: virtualColor({
|
||||||
|
name: "contrast",
|
||||||
|
dark: "contrastDark",
|
||||||
|
light: "contrastLight",
|
||||||
|
}),
|
||||||
|
secondaryContrast: virtualColor({
|
||||||
|
name: "secondaryContrast",
|
||||||
|
dark: "secondaryContrastDark",
|
||||||
|
light: "secondaryContrastLight",
|
||||||
|
}),
|
||||||
|
accent: virtualColor({
|
||||||
|
name: "accent",
|
||||||
|
dark: "blue",
|
||||||
|
light: "blue",
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const Wrapper: React.FunctionComponent<WrapperProps> = (props) => {
|
||||||
|
const [currentTaskData, setTaskData] = useState<TaskData<MoleculeData>>();
|
||||||
|
const [currentTheme, setTheme] = useState<"light" | "dark">("dark");
|
||||||
|
// Handle messages from parent
|
||||||
|
useEffect(() => {
|
||||||
|
window.addEventListener("message", (event) => {
|
||||||
|
const { type, data } = event.data;
|
||||||
|
console.log(event.data);
|
||||||
|
|
||||||
|
setTaskData(data.taskData);
|
||||||
|
setTheme(data.theme);
|
||||||
|
});
|
||||||
|
}, [props.mode]);
|
||||||
|
|
||||||
|
if (props.mode === "List") {
|
||||||
|
return (
|
||||||
|
<MantineProvider
|
||||||
|
colorSchemeManager={colorSchemeManager}
|
||||||
|
theme={theme}
|
||||||
|
forceColorScheme={currentTheme}
|
||||||
|
>
|
||||||
|
<ListItem data={currentTaskData} />
|
||||||
|
</MantineProvider>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<MantineProvider
|
||||||
|
colorSchemeManager={colorSchemeManager}
|
||||||
|
theme={theme}
|
||||||
|
forceColorScheme={currentTheme}
|
||||||
|
>
|
||||||
|
<MoleculeEditorPage data={currentTaskData} setData={setData} />
|
||||||
|
</MantineProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Wrapper;
|
||||||
23
frontend-plugin/tsconfig.json
Normal file
23
frontend-plugin/tsconfig.json
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es2020",
|
||||||
|
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"declaration": false,
|
||||||
|
"outDir": "./dist",
|
||||||
|
"sourceMap": false,
|
||||||
|
"noEmit": true,
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"],
|
||||||
|
"exclude": ["node_modules", "dist"],
|
||||||
|
"references": [{ "path": "./tsconfig.node.json" }],
|
||||||
|
}
|
||||||
11
frontend-plugin/tsconfig.node.json
Normal file
11
frontend-plugin/tsconfig.node.json
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"strict": true,
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"],
|
||||||
|
}
|
||||||
20
frontend-plugin/vite.config.ts
Normal file
20
frontend-plugin/vite.config.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { defineConfig } from "vite";
|
||||||
|
import react from "@vitejs/plugin-react";
|
||||||
|
import { viteSingleFile } from "vite-plugin-singlefile";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react(), viteSingleFile()],
|
||||||
|
define: {
|
||||||
|
"process.env.NODE_ENV": JSON.stringify("production"),
|
||||||
|
},
|
||||||
|
build: {
|
||||||
|
outDir: "dist",
|
||||||
|
emptyOutDir: true,
|
||||||
|
minify: "esbuild",
|
||||||
|
sourcemap: false,
|
||||||
|
assetsInlineLimit: 100000000, // Inline all assets
|
||||||
|
rollupOptions: {
|
||||||
|
input: "./index.html",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user