diff --git a/.gitignore b/.gitignore
index e102be8..a8baa10 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,5 @@
+node_modules
+package-lock.json
# postman
.postman
postman
diff --git a/Dockerfile b/backend/Dockerfile
similarity index 100%
rename from Dockerfile
rename to backend/Dockerfile
diff --git a/docker-compose.yaml b/backend/docker-compose.yaml
similarity index 100%
rename from docker-compose.yaml
rename to backend/docker-compose.yaml
diff --git a/requirements.txt b/backend/requirements.txt
similarity index 100%
rename from requirements.txt
rename to backend/requirements.txt
diff --git a/src/app.py b/backend/src/app.py
similarity index 100%
rename from src/app.py
rename to backend/src/app.py
diff --git a/src/logging_config.py b/backend/src/logging_config.py
similarity index 100%
rename from src/logging_config.py
rename to backend/src/logging_config.py
diff --git a/src/request_response_models.py b/backend/src/request_response_models.py
similarity index 100%
rename from src/request_response_models.py
rename to backend/src/request_response_models.py
diff --git a/frontend-plugin/index.html b/frontend-plugin/index.html
new file mode 100644
index 0000000..360e96f
--- /dev/null
+++ b/frontend-plugin/index.html
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend-plugin/package.json b/frontend-plugin/package.json
new file mode 100644
index 0000000..e7cd775
--- /dev/null
+++ b/frontend-plugin/package.json
@@ -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"
+ }
+}
diff --git a/frontend-plugin/src/EditorPage/CodeTextArea/CodeTextArea.tsx b/frontend-plugin/src/EditorPage/CodeTextArea/CodeTextArea.tsx
new file mode 100644
index 0000000..7688d2d
--- /dev/null
+++ b/frontend-plugin/src/EditorPage/CodeTextArea/CodeTextArea.tsx
@@ -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 (
+
+ );
+}
diff --git a/frontend-plugin/src/EditorPage/EditorPage.tsx b/frontend-plugin/src/EditorPage/EditorPage.tsx
new file mode 100644
index 0000000..b4e21fe
--- /dev/null
+++ b/frontend-plugin/src/EditorPage/EditorPage.tsx
@@ -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) {
+ const [text, setText] = useState(props.data?.data.text);
+ const [selectedAtom, setSelectedAtom] = useState(null);
+ const theme = useMantineTheme();
+
+ return (
+ <>
+
+
+
+
+
+
+ }>
+ Код
+
+ }>
+ Список
+
+
+
+
+ {
+ setText(text);
+ props.setData({ text: text });
+ }}
+ />
+
+
+
+ <>>
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+}
+
+export default MoleculeEditorPage;
diff --git a/frontend-plugin/src/EditorPage/MoleculePage.css b/frontend-plugin/src/EditorPage/MoleculePage.css
new file mode 100644
index 0000000..c15dc4f
--- /dev/null
+++ b/frontend-plugin/src/EditorPage/MoleculePage.css
@@ -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);
+}
diff --git a/frontend-plugin/src/EditorPage/MoleculeViewer/MoleculeViewer.css b/frontend-plugin/src/EditorPage/MoleculeViewer/MoleculeViewer.css
new file mode 100644
index 0000000..638a1a8
--- /dev/null
+++ b/frontend-plugin/src/EditorPage/MoleculeViewer/MoleculeViewer.css
@@ -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;
+}
diff --git a/frontend-plugin/src/EditorPage/MoleculeViewer/MoleculeViewer.tsx b/frontend-plugin/src/EditorPage/MoleculeViewer/MoleculeViewer.tsx
new file mode 100644
index 0000000..c777370
--- /dev/null
+++ b/frontend-plugin/src/EditorPage/MoleculeViewer/MoleculeViewer.tsx
@@ -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(null);
+ const molViewerRef = useRef(null);
+
+ const [isResizing, setIsResizing] = useState(false);
+
+ const [viewingData, setViewingData] = useState(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: ,
+ style: { paddingLeft: "5px" },
+ });
+ });
+ }
+ miew.setOptions({
+ settings: {
+ bg: {
+ color: theme.colors.secondaryDark[7],
+ },
+ fogAlpha: 0.7,
+ axes: true,
+ },
+ });
+ };
+
+ //мемоизируем объект чтобы он не перегружаля при изменении [miew] состояния
+ const MemoViewer = useMemo(
+ () => ,
+ [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 (
+
+
+ {MemoViewer}
+ {isResizing && viewingData && (
+
+ )}
+
+ {props.selectedAtom && (
+
+
+
+ )}
+
+
+
+
+
+
+
+ );
+}
+
+export default MoleculeViewer;
diff --git a/frontend-plugin/src/EditorPage/MoleculeViewer/MoleculeViewerMenu.tsx b/frontend-plugin/src/EditorPage/MoleculeViewer/MoleculeViewerMenu.tsx
new file mode 100644
index 0000000..a493d47
--- /dev/null
+++ b/frontend-plugin/src/EditorPage/MoleculeViewer/MoleculeViewerMenu.tsx
@@ -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 (
+ <>
+
+ <>
+
+ Атом: {props.selectedAtom.name} ({props.selectedAtom.serial})
+
+
+
Положение X: {props.selectedAtom.position.x}
+
Положение Y: {props.selectedAtom.position.y}
+
Положение Z: {props.selectedAtom.position.z}
+ >
+
+ >
+ );
+}
+
+export default MoleculeViewerMenu;
diff --git a/frontend-plugin/src/ListItem/ListItem.tsx b/frontend-plugin/src/ListItem/ListItem.tsx
new file mode 100644
index 0000000..b3b2fa5
--- /dev/null
+++ b/frontend-plugin/src/ListItem/ListItem.tsx
@@ -0,0 +1,23 @@
+// ListItem/ListItem.tsx
+import React from "react";
+import { MoleculeData, TaskData } from "../Types/plugin";
+
+const ListItem: React.FunctionComponent<{
+ data: TaskData | undefined;
+}> = (props) => {
+ return (
+
+
{props.data?.name}
+
+ {props.data?.description}
+
+
+ Value: {props.data?.data.text || "Not set"}
+
+
+ );
+};
+
+export default ListItem;
diff --git a/frontend-plugin/src/Types/plugin.tsx b/frontend-plugin/src/Types/plugin.tsx
new file mode 100644
index 0000000..ed802a5
--- /dev/null
+++ b/frontend-plugin/src/Types/plugin.tsx
@@ -0,0 +1,28 @@
+// Types
+export interface TaskData {
+ id: number;
+ name: string;
+ description: string;
+ data: TData;
+}
+
+export interface TaskEditorProps {
+ data: TaskData | undefined;
+ setData: (data: TData) => void;
+}
+
+export interface TaskTypePlugin {
+ type: string;
+ ListItem: React.ComponentType | undefined>;
+ Editor: React.ComponentType>;
+}
+
+export interface MoleculeData {
+ text: string;
+}
+
+export interface SelectedAtom {
+ serial: number;
+ name: string;
+ position: { x: number; y: number; z: number };
+}
diff --git a/frontend-plugin/src/index.css b/frontend-plugin/src/index.css
new file mode 100644
index 0000000..6d03b98
--- /dev/null
+++ b/frontend-plugin/src/index.css
@@ -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;
+}
diff --git a/frontend-plugin/src/index.tsx b/frontend-plugin/src/index.tsx
new file mode 100644
index 0000000..01b24b8
--- /dev/null
+++ b/frontend-plugin/src/index.tsx
@@ -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();
+}
+
+// Notify parent that plugin is ready
+if (window.parent !== window) {
+ window.parent.postMessage({ type: "plugin-ready" }, "*");
+}
diff --git a/frontend-plugin/src/wrapper.tsx b/frontend-plugin/src/wrapper.tsx
new file mode 100644
index 0000000..07454b3
--- /dev/null
+++ b/frontend-plugin/src/wrapper.tsx
@@ -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 = (props) => {
+ const [currentTaskData, setTaskData] = useState>();
+ 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 (
+
+
+
+ );
+ } else {
+ return (
+
+
+
+ );
+ }
+};
+
+export default Wrapper;
diff --git a/frontend-plugin/tsconfig.json b/frontend-plugin/tsconfig.json
new file mode 100644
index 0000000..31b2acf
--- /dev/null
+++ b/frontend-plugin/tsconfig.json
@@ -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" }],
+}
diff --git a/frontend-plugin/tsconfig.node.json b/frontend-plugin/tsconfig.node.json
new file mode 100644
index 0000000..ef7bfa3
--- /dev/null
+++ b/frontend-plugin/tsconfig.node.json
@@ -0,0 +1,11 @@
+{
+ "compilerOptions": {
+ "composite": true,
+ "skipLibCheck": true,
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "allowSyntheticDefaultImports": true,
+ "strict": true,
+ },
+ "include": ["vite.config.ts"],
+}
diff --git a/frontend-plugin/vite.config.ts b/frontend-plugin/vite.config.ts
new file mode 100644
index 0000000..ec1dd55
--- /dev/null
+++ b/frontend-plugin/vite.config.ts
@@ -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",
+ },
+ },
+});