Initial commit

This commit is contained in:
2026-03-16 22:05:19 +03:00
commit 8e78b8cb47
71 changed files with 7846 additions and 0 deletions

View File

@@ -0,0 +1,58 @@
.tile {
--bgsize: 7px;
--bgoffset: 7px;
width: 25vw;
min-width: 275px;
max-width: 300px;
margin: 0px 10px;
padding: 20px 30px;
border: 1px solid;
border-color: var(--mantine-color-contrast-filled);
border-radius: var(--paper-radius);
transform: translateY(0px);
overflow: visible;
/*Move up on hover*/
&:hover {
transform: scale(105%);
}
/*Background 45 degree lines */
&::before,
&::after {
border-radius: var(--paper-radius);
background-image: linear-gradient(
45deg,
rgba(255, 255, 255, 0) 33.33%,
var(--mantine-color-contrast-filled) 33.33%,
var(--mantine-color-contrast-filled) 50%,
rgba(255, 255, 255, 0) 50%,
rgba(255, 255, 255, 0) 83.33%,
var(--mantine-color-contrast-filled) 83.33%,
var(--mantine-color-contrast-filled) 100%
);
content: "";
display: block;
position: absolute;
z-index: -2;
top: var(--bgoffset);
left: var(--bgoffset);
width: 100%;
height: 100%;
background-size: var(--bgsize) var(--bgsize);
}
&::before {
top: 0px;
left: 0px;
background: var(--mantine-color-body);
z-index: -1;
}
}
/*make it so the button is on bottom of card*/
.card_text {
flex-grow: 1;
}

View File

@@ -0,0 +1,36 @@
import { Card, Text, Button, Title, Space } from "@mantine/core";
import "./CardWithButton.css";
import { Link } from "react-router";
import CustomButton from "Components/CustomButton/CustomButton";
interface ButtonWithTextProps {
title: string;
text: string;
buttonText: string;
link: string;
}
function ButtonWithText(props: ButtonWithTextProps) {
return (
<Card className="tile" shadow="sm" padding="lg" radius="md" withBorder>
<Text fw={650} mb="sm" size="md">
{props.title}
</Text>
<Text size="sm" c="dimmed" className="card_text">
{props.text}
</Text>
<Space h="md" />
<Link to={props.link} className="invisible_link">
<CustomButton
color="contrast"
text={props.buttonText}
style="color"
textAlign="center"
textSize="lg"
/>
</Link>
</Card>
);
}
export default ButtonWithText;

View 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>
);
}

View File

@@ -0,0 +1,105 @@
.colored {
border: 2px solid transparent;
border-radius: 5px;
width: 100%;
display: flex;
padding-left: 5px;
padding-right: 5px;
}
.colored:hover {
background-color: color;
}
.textAlign-left {
text-align: left;
justify-content: left;
}
.textAlign-right {
text-align: right;
justify-content: right;
}
.textAlign-center {
text-align: center;
justify-content: center;
justify-items: center;
* {
align-content: center;
}
}
/* Variants */
.outline {
border-color: var(--color);
background: transparent;
color: var(--color);
}
.outline:hover {
background: color-mix(in srgb, var(--hovercolor) 20%, transparent);
}
/* ###################### */
.color {
background: var(--color);
color: var(--textcolor);
}
.color:hover {
background-color: var(--hovercolor);
}
.primary {
--color: var(--mantine-color-primary-filled);
--textcolor: var(--mantine-color-contrast-filled);
--hovercolor: var(--mantine-color-secondary-filled);
}
.secondary {
--color: var(--mantine-color-secondary-filled);
--textcolor: var(--mantine-color-secondaryContrast-filled);
--hovercolor: var(--mantine-color-primary-filled);
}
.contrast {
--color: var(--mantine-color-contrast-filled);
--textcolor: var(--mantine-color-primary-filled);
--hovercolor: var(--mantine-color-secondaryContrast-filled);
}
.secondary-contrast {
--color: var(--mantine-color-secondaryContrast-filled);
--textcolor: var(--mantine-color-secondary-filled);
--hovercolor: var(--mantine-color-contrast-filled);
}
.accent {
--color: var(--mantine-color-accent-filled);
--textcolor: white;
--hovercolor: var(--mantine-color-accent-filled-hover);
}
.warning {
--color: var(--mantine-color-yellow-filled);
--textcolor: white;
--hovercolor: var(--mantine-color-yellow-filled-hover);
}
.error {
--color: var(--mantine-color-red-filled);
--textcolor: white;
--hovercolor: var(--mantine-color-red-filled-hover);
}
/* ###################### */
.subtle {
color: var(--color);
}
.subtle:hover {
background-color: color-mix(in srgb, var(--color) 20%, transparent);
}

View File

@@ -0,0 +1,62 @@
import { Space, Text, UnstyledButton, type MantineSize } from "@mantine/core";
import type { ReactElement } from "react";
import "./CustomButton.css";
interface CustomButtonProps {
text?: string;
icon?: ReactElement;
style?: "outline" | "color" | "subtle";
color?:
| "primary"
| "secondary"
| "contrast"
| "secondary-cotrast"
| "accent"
| "warning"
| "error"
| string;
textAlign?: "left" | "center" | "right";
textSize?: MantineSize;
onClick?: React.MouseEventHandler;
disabled?: boolean;
}
function CustomButton({
text,
icon,
style = "color",
color = "contrast",
textAlign = "center",
textSize = "md",
onClick = () => {},
disabled = false,
}: CustomButtonProps) {
console.log(color);
return (
<UnstyledButton
className={`colored ${style} ${color} textAlign-${textAlign}`}
style={
color !== "primary" &&
color !== "secondary" &&
color !== "contrast" &&
color !== "secondary-contrast" &&
color !== "accent" &&
color !== "warning" &&
color !== "error"
? {
"--color": color,
"--textcolor": "white",
"--hovercolor": `color-mix(in srgb,${color} 90%, black)`,
}
: {}
}
onClick={onClick}
disabled={disabled}
>
{icon} {icon && <Space w="sm" />}
<Text size={textSize}>{text}</Text>
</UnstyledButton>
);
}
export default CustomButton;

View File

@@ -0,0 +1,23 @@
.logo {
height: 80%;
}
.headerLogo {
height: 100%;
display: flex;
flex-direction: row;
align-items: center;
gap: 10px;
font-family: "Quicking";
letter-spacing: 1px;
cursor: pointer;
}
.header {
height: 100%;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
padding: 0px 5px;
}

View File

@@ -0,0 +1,33 @@
import { Burger, Image, Text } from "@mantine/core";
import "./Header.css";
import { useLayoutStore } from "Stores/LayoutStore";
import { Link } from "react-router";
import { baseUrl } from "GlobalVars";
import { routes } from "Routes/Routes";
function Header() {
const { is_navbar_open, set_navbar_open } = useLayoutStore();
return (
<div className="header">
<Link className="headerLogo invisible_link" to={routes.MainPage.path}>
<Image
className="logo"
w="auto"
fit="contain"
src={baseUrl + "/bitmap.png"}
alt="image"
/>
<Text size="xl">QMolSim</Text>
</Link>
<Burger
aria-label="sidebar toggle"
opened={is_navbar_open}
onClick={() => {
set_navbar_open(!is_navbar_open);
}}
/>
</div>
);
}
export default Header;

View File

@@ -0,0 +1,64 @@
.sidebar {
height: 100%;
margin-top: 10px;
margin-bottom: 25px;
margin-left: 10px;
margin-right: 10px;
justify-content: space-between;
display: flex;
flex-direction: column;
}
.selected {
background-color: red;
}
.sidebar_top {
gap: 5px;
}
.sidebar_bottom {
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
width: 100%;
}
.sidebar_bottom_top {
display: flex;
flex-direction: row;
align-items: center;
min-width: 0px;
margin-bottom: 10px;
}
.sidebar_bottom_bottom {
display: flex;
flex-direction: row;
width: 100%;
justify-content: space-evenly;
min-width: 0px;
gap: 10px;
}
.userInfo {
padding-left: 15px;
gap: 5px;
min-width: 0px;
}
.sidebar_bottom_button {
padding-left: 10px;
padding-right: 10px;
padding-top: 3px;
padding-bottom: 3px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
}
.sidebar_bottom_button:hover {
background-color: var(--mantine-color-secondary-filled);
}

View File

@@ -0,0 +1,165 @@
import {
Avatar,
Button,
Divider,
Modal,
Stack,
Text,
Title,
UnstyledButton,
useMantineTheme,
} from "@mantine/core";
import "./Sidebar.css";
import {
IconDevicesPc,
IconFileDescription,
IconFlaskFilled,
IconLogout,
IconSettings2,
IconUsers,
type Icon,
type IconProps,
} from "@tabler/icons-react";
import { Link, useLocation } from "react-router";
import { useState, type ForwardRefExoticComponent } from "react";
import { routes } from "Routes/Routes";
import keycloak from "Api/Keycloak/Keycloak";
import { AuthenticationStore } from "Stores/AuthenticationStore";
import { baseUri, baseUrl } from "GlobalVars";
import CustomButton from "Components/CustomButton/CustomButton";
interface SubtleLinkButtonProps {
link: string;
Icon: ForwardRefExoticComponent<IconProps & React.RefAttributes<Icon>>;
text: string;
color: string;
selected?: boolean;
}
function SubtleLinkButton(props: SubtleLinkButtonProps) {
return (
<Link to={props.link} className="invisible_link">
<CustomButton
icon={<props.Icon color={props.color} size={20} />}
style="subtle"
color={props.selected ? props.color : "contrast"}
text={props.text}
textAlign="left"
/>
</Link>
);
}
function Sidebar() {
const { profile } = AuthenticationStore();
const [open, set_open] = useState(false);
const theme = useMantineTheme();
const location = useLocation(); // get current URL
return (
<div className="sidebar">
<Modal
opened={open}
onClose={() => set_open(false)}
title=<Title size="lg">Выйти?</Title>
centered
withCloseButton={false}
size="auto"
>
<div className="sidebar_bottom_bottom">
<CustomButton
text="Подтвердить"
onClick={() => keycloak.logout({ redirectUri: baseUri + baseUrl })}
textSize="lg"
color="contrast"
></CustomButton>
<CustomButton
onClick={() => set_open(false)}
text="Отменить"
textSize="lg"
style="outline"
color="contrast"
></CustomButton>
</div>
</Modal>
<div className="sidebar_top">
<SubtleLinkButton
link={routes.ExperimentsPage.path}
Icon={IconFlaskFilled}
text="Эксперименты"
color={theme.colors.teal[7]}
selected={location.pathname.startsWith(routes.ExperimentsPage.path)}
/>
<SubtleLinkButton
link={routes.MachinesPage.path}
Icon={IconDevicesPc}
text="Вычислительные системы"
color={theme.colors.violet[7]}
selected={location.pathname.startsWith(routes.MachinesPage.path)}
/>
<SubtleLinkButton
link={routes.TeamsPage.path}
Icon={IconUsers}
text="Команды"
color={theme.colors.grape[7]}
selected={location.pathname.startsWith(routes.TeamsPage.path)}
/>
<SubtleLinkButton
link={routes.DocumentationPage.path}
Icon={IconFileDescription}
text="Документация"
color={theme.colors.blue[7]}
selected={location.pathname.startsWith(routes.DocumentationPage.path)}
/>
</div>
<div className="sidebar_bottom">
{keycloak.authenticated && (
<>
<div className="sidebar_bottom_top">
<Avatar radius="xl" />
<Stack className="userInfo">
<Text size="md">{profile?.username}</Text>
<Text size="sm" c="dimmed" truncate="end">
{profile?.email}
</Text>
</Stack>
</div>
<Divider
color="secondaryContrast"
style={{ width: "100%", height: "5px", margin: "5px" }}
/>
<div className="sidebar_bottom_bottom">
<Link to={routes.SettingsPage.path} className="invisible_link">
<CustomButton
style="subtle"
icon={<IconSettings2 size={26} />}
text="Настройки"
color="contrast"
textSize="lg"
/>
</Link>
<CustomButton
style="subtle"
icon={<IconLogout size={26} />}
text="Выйти"
textSize="lg"
color="contrast"
onClick={() => {
set_open(true);
}}
/>
</div>
</>
)}
{!keycloak.authenticated && (
<UnstyledButton onClick={() => keycloak.login()}>
Войти / Зарегестрироваться
</UnstyledButton>
)}
</div>
</div>
);
}
export default Sidebar;

View File

@@ -0,0 +1,50 @@
.ExperimentsListCard {
height: 120px;
padding: 12px;
}
.ExperimentSectionWithLine {
border-right: 1px solid var(--mantine-color-contrast-filled);
padding-right: 10px;
}
.ExperimentPill {
background-color: var(--mantine-color-contrast-filled);
color: var(--mantine-color-primary-filled);
}
.ExperimentPill2 {
border: 2px solid var(--mantine-color-accent-filled);
background-color: transparent;
color: var(--mantine-color-accent-filled);
* {
width: 100%;
text-align: center;
}
}
.dateContainer {
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: right;
gap: 5px;
.p {
text-wrap: nowrap;
}
}
.TopRightContainer {
justify-content: right;
display: flex;
flex-direction: row;
flex-wrap: nowrap;
gap: 15px;
justify-items: center;
}
.RightExperimentSection {
text-align: right;
padding-right: 10px;
display: flex;
flex-direction: column;
}

View File

@@ -0,0 +1,120 @@
import { Card, Pill, SimpleGrid, Text, UnstyledButton } from "@mantine/core";
import "./ExperimentsListCard.css";
import { useNavigate } from "react-router";
import type { Experiment, TaskData } from "Types/Experiment/Experiment";
import { IconTrash } from "@tabler/icons-react";
import type { MouseEvent } from "react";
import { useExperimentStore } from "Stores/ExperimentStore";
function ExperimentsListCard(props: Experiment) {
const navigate = useNavigate();
const { selectExperiment, removeExperiment, tasks, teams } =
useExperimentStore();
const team = teams.find((team) => {
return team.id == props.team_id;
});
const experiment_tasks = tasks.filter((a) => a.id in props.tasks_ids);
const handleDelete = () => {
removeExperiment(props.id);
};
return (
<Card
className="ExperimentsListCard"
onClick={() => {
console.log(props.id);
selectExperiment(props.id);
navigate(props.id.toString());
}}
>
<SimpleGrid cols={3}>
<div className="ExperimentSectionWithLine">
<Text mb="sm" size="md" style={{ textDecorationLine: "underline" }}>
{props.name}
</Text>
<Text mb="sm" size="md">
Team: {team ? team.name : "ERROR"}
</Text>
<Pill className="ExperimentPill">
<Text mb="sm" size="md">
Статус: {props.experiment_status}
</Text>
</Pill>
</div>
<div className="ExperimentSectionWithLine">
<SimpleGrid cols={2} verticalSpacing="0px">
<Text>Задачи:</Text>
{experiment_tasks.map((task: TaskData<any>) => {
return (
<Text
size="md"
style={{
textWrap: "nowrap",
textOverflow: "ellipsis",
overflow: "hidden",
}}
>
{task.name}
</Text>
);
})}
{experiment_tasks.length == 0 ? (
<Pill
size="md"
className="ExperimentPill2"
style={{
textWrap: "nowrap",
textOverflow: "ellipsis",
overflow: "hidden",
}}
>
Нет Задач
</Pill>
) : (
<></>
)}
</SimpleGrid>
</div>
<div className="RightExperimentSection">
<div className="TopRightContainer">
<div className="dateContainer">
<Text size="sm">{props.date_created.toLocaleDateString()} </Text>
<Text size="sm">{props.date_created.toLocaleTimeString()}</Text>
</div>
<UnstyledButton
onClick={(e: MouseEvent) => {
e.stopPropagation();
handleDelete();
}}
style={{ cursor: "pointer" }}
>
<IconTrash size={20} />
</UnstyledButton>
</div>
<div
style={{
flexGrow: 1,
display: "flex",
justifyContent: "right",
alignItems: "center",
}}
>
<Text>
{" "}
Тип эксперимента:{" "}
<Pill className="ExperimentPill2">{props.experiment_type}</Pill>
</Text>
</div>
<Text color="secondary" size="sm">
#{props.id}
</Text>
</div>
</SimpleGrid>
</Card>
);
}
export default ExperimentsListCard;

View File

@@ -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;
}

View 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 type { SelectedAtom } from "Types/Experiment/MoleculeEdit/MoleculeEdit";
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 size={25} />
</UnstyledButton>
</div>
</div>
);
}
export default MoleculeViewer;

View File

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

View File

@@ -0,0 +1,16 @@
.PaginationContainer {
width: 100%;
flex: 1;
position: relative;
display: flex;
flex-direction: column;
align-items: center;
}
.PaginationContents {
flex-grow: 1;
width: 100%;
display: flex;
flex-direction: column;
gap: 10px;
}

View File

@@ -0,0 +1,12 @@
import { Pagination } from "@mantine/core";
import "./PaginationContainer.css";
import type { PropsWithChildren } from "react";
export function PaginationContainer(a: PropsWithChildren) {
return (
<div className="PaginationContainer">
<div className="PaginationContents">{a.children}</div>
<Pagination total={10} color="accent" />
</div>
);
}

View File

@@ -0,0 +1,45 @@
.background-light {
width: 100%;
height: 100%;
overflow: hidden;
background: white
linear-gradient(
135deg,
rgba(110, 156, 223, 0.125) 0%,
rgba(110, 156, 223, 0.125) 14.286%,
rgba(110, 156, 223, 0.25) 14.286%,
rgba(110, 156, 223, 0.25) 28.571%,
rgba(110, 156, 223, 0.375) 28.571%,
rgba(110, 156, 223, 0.375) 42.857%,
rgba(110, 156, 223, 0.5) 42.857%,
rgba(110, 156, 223, 0.5) 57.143%,
rgba(110, 156, 223, 0.725) 57.143%,
rgba(110, 156, 223, 0.725) 71.429%,
rgba(110, 156, 223, 0.85) 71.429%,
rgba(110, 156, 223, 0.85) 85.714%,
rgba(110, 156, 223, 1) 85.714% 100%
);
}
.background-dark {
width: 100%;
height: 100%;
overflow: hidden;
background: black
linear-gradient(
135deg,
rgba(36, 54, 162, 0.353) 0%,
rgba(36, 54, 162, 0.353) 14.286%,
rgba(36, 54, 162, 0.5) 14.286%,
rgba(36, 54, 162, 0.5) 28.571%,
rgba(36, 54, 162, 0.612) 28.571%,
rgba(36, 54, 162, 0.612) 42.857%,
rgba(36, 54, 162, 0.707) 42.857%,
rgba(36, 54, 162, 0.707) 57.143%,
rgba(36, 54, 162, 0.851) 57.143%,
rgba(36, 54, 162, 0.851) 71.429%,
rgba(36, 54, 162, 0.921) 71.429%,
rgba(36, 54, 162, 0.921) 85.714%,
rgba(36, 54, 162, 1) 85.714% 100%
);
}

View File

@@ -0,0 +1,13 @@
import { useMantineColorScheme } from "@mantine/core";
import "./Background.css";
function Background() {
const { colorScheme } = useMantineColorScheme();
return (
<div
className={colorScheme == "dark" ? "background-dark" : "background-light"}
/>
);
}
export default Background;