migrated the frontend file generation, changed folder structure
This commit is contained in:
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