diff --git a/app/package-lock.json b/app/package-lock.json
index 8dfd6e7..45a89a1 100644
--- a/app/package-lock.json
+++ b/app/package-lock.json
@@ -8,6 +8,7 @@
"name": "react",
"version": "0.0.0",
"dependencies": {
+ "chart.js": "^4.4.8",
"path": "^0.12.7",
"react": "^19.0.0",
"react-dom": "^19.0.0",
@@ -1029,6 +1030,12 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
+ "node_modules/@kurkle/color": {
+ "version": "0.3.4",
+ "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz",
+ "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==",
+ "license": "MIT"
+ },
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -2159,6 +2166,18 @@
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
+ "node_modules/chart.js": {
+ "version": "4.4.8",
+ "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.8.tgz",
+ "integrity": "sha512-IkGZlVpXP+83QpMm4uxEiGqSI7jFizwVtF3+n5Pc3k7sMO+tkd0qxh2OzLhenM0K80xtmAONWGBn082EiBQSDA==",
+ "license": "MIT",
+ "dependencies": {
+ "@kurkle/color": "^0.3.0"
+ },
+ "engines": {
+ "pnpm": ">=8"
+ }
+ },
"node_modules/chokidar": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
diff --git a/app/package.json b/app/package.json
index 07751c7..abba5c4 100644
--- a/app/package.json
+++ b/app/package.json
@@ -10,6 +10,7 @@
"preview": "vite preview"
},
"dependencies": {
+ "chart.js": "^4.4.8",
"path": "^0.12.7",
"react": "^19.0.0",
"react-dom": "^19.0.0",
diff --git a/app/src/components/icons/ExportModuleIcons.tsx b/app/src/components/icons/ExportModuleIcons.tsx
index e2547d4..ffa64c4 100644
--- a/app/src/components/icons/ExportModuleIcons.tsx
+++ b/app/src/components/icons/ExportModuleIcons.tsx
@@ -100,3 +100,34 @@ export function VisualizationIcon({ isActive }: { isActive: boolean }) {
);
}
+
+export function CartIcon({ isActive }: { isActive: boolean }) {
+ return (
+
+
+
+
+
+
+ );
+}
diff --git a/app/src/components/icons/RealTimeVisulationIcons.tsx b/app/src/components/icons/RealTimeVisulationIcons.tsx
new file mode 100644
index 0000000..0174eda
--- /dev/null
+++ b/app/src/components/icons/RealTimeVisulationIcons.tsx
@@ -0,0 +1,162 @@
+export function CleanPannel() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+export function EyeIcon() {
+ return (
+
+
+
+
+ );
+}
+
+export function LockIcon() {
+ return (
+
+
+
+ );
+}
+
+export function PlayIcon() {
+ return (
+
+
+
+ );
+}
+
+export function CommentIcon() {
+ return (
+
+
+
+
+
+ );
+}
+
+export function SaveTeemplateIcon() {
+ return (
+
+
+
+
+ );
+}
diff --git a/app/src/components/layout/sidebarLeft/SideBarLeft.tsx b/app/src/components/layout/sidebarLeft/SideBarLeft.tsx
index f6b0842..fb7496a 100644
--- a/app/src/components/layout/sidebarLeft/SideBarLeft.tsx
+++ b/app/src/components/layout/sidebarLeft/SideBarLeft.tsx
@@ -5,21 +5,31 @@ import Header from "./Header";
import useToggleStore from "../../../store/useUIToggleStore";
import Assets from "./Assets";
import useModuleStore from "../../../store/useModuleStore";
+import Widgets from "./visualization/widgets/Widgets";
+import Templates from "./visualization/Templates";
+import Search from "../../ui/inputs/Search";
const SideBarLeft: React.FC = () => {
- const [activeOption, setActiveOption] = useState("Outline");
+ const [activeOption, setActiveOption] = useState("Widgets");
const { toggleUI } = useToggleStore();
const { activeModule } = useModuleStore();
- // Reset activeList whenever activeModule changes
+ // Reset activeOption whenever activeModule changes
useEffect(() => {
setActiveOption("Outline");
+ if (activeModule === "visualization") setActiveOption("Widgets");
}, [activeModule]);
const handleToggleClick = (option: string) => {
setActiveOption(option); // Update the active option
};
+
+ const handleSearchChange = (value: string) => {
+ // Log the search value for now
+ console.log(value);
+ };
+
return (
@@ -28,11 +38,17 @@ const SideBarLeft: React.FC = () => {
{activeModule === "visualization" ? (
<>
+
+
+ {activeOption === "Widgets" ? : }
+
>
+ ) : activeModule === "market" ? (
+ <>>
) : (
<>
{
+ const { templates, removeTemplate } = useTemplateStore();
+ const { setSelectedZone } = useSelectedZoneStore();
+
+ console.log('templates: ', templates);
+ const handleDeleteTemplate = (id: string) => {
+ removeTemplate(id);
+ };
+
+ const handleLoadTemplate = (template: any) => {
+ setSelectedZone((prev) => ({
+ ...prev,
+ panelOrder: template.panelOrder,
+ activeSides: Array.from(new Set([...prev.activeSides, ...template.panelOrder])),
+ widgets: template.widgets,
+ }));
+ };
+
+ return (
+
+ {templates.map((template) => (
+
+ {template.snapshot && (
+
{/* 16:9 aspect ratio */}
+
handleLoadTemplate(template)}
+ />
+
+ )}
+
+
handleLoadTemplate(template)}
+ style={{
+ cursor: 'pointer',
+ fontWeight: '500',
+ // ':hover': {
+ // textDecoration: 'underline'
+ // }
+ }}
+ >
+ {template.name}
+
+
handleDeleteTemplate(template.id)}
+ style={{
+ padding: '0.25rem 0.5rem',
+ background: '#ff4444',
+ color: 'white',
+ border: 'none',
+ borderRadius: '4px',
+ cursor: 'pointer',
+ transition: 'opacity 0.3s ease',
+ // ':hover': {
+ // opacity: 0.8
+ // }
+ }}
+ aria-label="Delete template"
+ >
+ Delete
+
+
+
+ ))}
+ {templates.length === 0 && (
+
+ No saved templates yet. Create one in the visualization view!
+
+ )}
+
+ );
+};
+
+export default Templates;
+
diff --git a/app/src/components/layout/sidebarLeft/visualization/widgets/ChartComponent.tsx b/app/src/components/layout/sidebarLeft/visualization/widgets/ChartComponent.tsx
new file mode 100644
index 0000000..39da08d
--- /dev/null
+++ b/app/src/components/layout/sidebarLeft/visualization/widgets/ChartComponent.tsx
@@ -0,0 +1,130 @@
+import React, { useEffect, useRef, useMemo } from "react";
+import { Chart } from "chart.js/auto";
+import { useThemeStore } from "../../../../../store/useThemeStore";
+
+// Define Props Interface
+interface ChartComponentProps {
+ type: any; // Type of chart (e.g., "bar", "line", etc.)
+ title: string; // Title of the chart
+ fontFamily?: string; // Optional font family for the chart title
+ fontSize?: string; // Optional font size for the chart title
+ fontWeight?: "Light" | "Regular" | "Bold"; // Optional font weight for the chart title
+ data: {
+ labels: string[]; // Labels for the x-axis
+ datasets: {
+ data: number[]; // Data points for the chart
+ backgroundColor: string; // Background color for the chart
+ borderColor: string; // Border color for the chart
+ borderWidth: number; // Border width for the chart
+ }[];
+ }; // Data for the chart
+}
+
+const ChartComponent = ({
+ type,
+ title,
+ fontFamily,
+ fontSize,
+ fontWeight = "Regular", // Default to "Regular"
+ data: propsData,
+}: ChartComponentProps) => {
+ const canvasRef = useRef(null);
+ const { themeColor } = useThemeStore();
+
+ // Memoize Theme Colors to Prevent Unnecessary Recalculations
+ const buttonActionColor = useMemo(
+ () => themeColor[0] || "#5c87df",
+ [themeColor]
+ );
+ const buttonAbortColor = useMemo(
+ () => themeColor[1] || "#ffffff",
+ [themeColor]
+ );
+
+ // Memoize Font Weight Mapping
+ const chartFontWeightMap = useMemo(
+ () => ({
+ Light: "lighter" as const,
+ Regular: "normal" as const,
+ Bold: "bold" as const,
+ }),
+ []
+ );
+
+ // Parse and Memoize Font Size
+ const fontSizeValue = useMemo(
+ () => (fontSize ? parseInt(fontSize) : 12),
+ [fontSize]
+ );
+
+ // Determine and Memoize Font Weight
+ const fontWeightValue = useMemo(
+ () => chartFontWeightMap[fontWeight],
+ [fontWeight, chartFontWeightMap]
+ );
+
+ // Memoize Chart Font Style
+ const chartFontStyle = useMemo(
+ () => ({
+ family: fontFamily || "Arial",
+ size: fontSizeValue,
+ weight: fontWeightValue,
+ color: "#2B3344",
+ }),
+ [fontFamily, fontSizeValue, fontWeightValue]
+ );
+
+ // Memoize Chart Data
+ const data = useMemo(() => propsData, [propsData]);
+
+ // Memoize Chart Options
+ const options = useMemo(
+ () => ({
+ responsive: true,
+ maintainAspectRatio: false,
+ plugins: {
+ title: {
+ display: true,
+ text: title,
+ font: chartFontStyle,
+ align: "start", // Left align the title
+ padding: {
+ top: 10, // Add padding above the title
+ bottom: 20, // Add padding between the title and the chart
+ },
+ },
+ legend: {
+ display: false,
+ },
+ },
+ }),
+ [title, chartFontStyle]
+ );
+
+ // Initialize Chart on Component Mount
+ useEffect(() => {
+ if (!canvasRef.current) return;
+
+ const ctx = canvasRef.current.getContext("2d");
+ if (!ctx) return;
+
+ const chart = new Chart(ctx, { type, data, options });
+
+ // Cleanup: Destroy the chart instance when the component unmounts
+ return () => chart.destroy();
+ }, [type, data, options]); // Only recreate the chart when these dependencies change
+
+ return ;
+};
+
+export default React.memo(ChartComponent, (prevProps, nextProps) => {
+ // Custom comparison function to prevent unnecessary re-renders
+ return (
+ prevProps.type === nextProps.type &&
+ prevProps.title === nextProps.title &&
+ prevProps.fontFamily === nextProps.fontFamily &&
+ prevProps.fontSize === nextProps.fontSize &&
+ prevProps.fontWeight === nextProps.fontWeight &&
+ JSON.stringify(prevProps.data) === JSON.stringify(nextProps.data)
+ );
+});
diff --git a/app/src/components/layout/sidebarLeft/visualization/widgets/Widgets.tsx b/app/src/components/layout/sidebarLeft/visualization/widgets/Widgets.tsx
new file mode 100644
index 0000000..3fde4c8
--- /dev/null
+++ b/app/src/components/layout/sidebarLeft/visualization/widgets/Widgets.tsx
@@ -0,0 +1,28 @@
+import { useState } from "react";
+import ToggleHeader from "../../../../ui/inputs/ToggleHeader";
+import Widgets2D from "./Widgets2D";
+import Widgets3D from "./Widgets3D";
+import WidgetsTemplate from "./WidgetsTemplate";
+
+const Widgets = () => {
+ const [activeOption, setActiveOption] = useState("2D");
+
+ const handleToggleClick = (option: string) => {
+ setActiveOption(option);
+ };
+
+ return (
+
+
+ {activeOption === "2D" && }
+ {activeOption === "3D" && }
+ {activeOption === "Templates" && }
+
+ );
+};
+
+export default Widgets;
diff --git a/app/src/components/layout/sidebarLeft/visualization/widgets/Widgets2D.tsx b/app/src/components/layout/sidebarLeft/visualization/widgets/Widgets2D.tsx
new file mode 100644
index 0000000..eaa453e
--- /dev/null
+++ b/app/src/components/layout/sidebarLeft/visualization/widgets/Widgets2D.tsx
@@ -0,0 +1,138 @@
+import React from "react";
+import { useWidgetStore } from "../../../../../store/useWidgetStore";
+import { ChartType } from "chart.js/auto";
+import ChartComponent from "./ChartComponent";
+
+const chartTypes: ChartType[] = [
+ "bar",
+ "line",
+ "pie",
+ "doughnut",
+ "radar",
+ "polarArea",
+];
+
+const sampleData = {
+ labels: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul"],
+ datasets: [
+ {
+ data: [65, 59, 80, 81, 56, 55, 40],
+ backgroundColor: "#6f42c1",
+ borderColor: "#ffffff",
+ borderWidth: 1,
+ },
+ ],
+};
+
+interface WidgetProps {
+ type: ChartType;
+ index: number;
+ title: string;
+}
+
+const ChartWidget: React.FC = ({ type, index, title }) => {
+ const { setDraggedAsset } = useWidgetStore((state) => state);
+
+ return (
+ {
+ setDraggedAsset({
+ type,
+ id: `widget-${index + 1}`,
+ title,
+ panel: "top",
+ data: sampleData,
+ });
+ }}
+ onDragEnd={() => setDraggedAsset(null)}
+ >
+
+
+ );
+};
+
+const ProgressBarWidget = ({
+ id,
+ title,
+ data,
+}: {
+ id: string;
+ title: string;
+ data: any;
+}) => {
+ const { setDraggedAsset } = useWidgetStore((state) => state);
+
+ return (
+ {
+ setDraggedAsset({
+ type: "progress",
+ id,
+ title,
+ panel: "top",
+ data,
+ });
+ }}
+ onDragEnd={() => setDraggedAsset(null)}
+ >
+
{title}
+ {data.stocks.map((stock: any, index: number) => (
+
+
+
+ {stock.key}
+ {stock.value}
+
+ {stock.description}
+
+
Icon
+
+ ))}
+
+ );
+};
+
+const Widgets2D = () => {
+ return (
+
+
+ {chartTypes.map((type, index) => {
+ const widgetTitle = `Widget ${index + 1}`;
+ return (
+
+ );
+ })}
+
+
+
+
+ );
+};
+
+export default Widgets2D;
diff --git a/app/src/components/layout/sidebarLeft/visualization/widgets/Widgets3D.tsx b/app/src/components/layout/sidebarLeft/visualization/widgets/Widgets3D.tsx
new file mode 100644
index 0000000..9a14410
--- /dev/null
+++ b/app/src/components/layout/sidebarLeft/visualization/widgets/Widgets3D.tsx
@@ -0,0 +1,11 @@
+import React from 'react'
+
+const Widgets3D = () => {
+ return (
+
+ Widgets3D
+
+ )
+}
+
+export default Widgets3D
diff --git a/app/src/components/layout/sidebarLeft/visualization/widgets/WidgetsTemplate.tsx b/app/src/components/layout/sidebarLeft/visualization/widgets/WidgetsTemplate.tsx
new file mode 100644
index 0000000..14ab07e
--- /dev/null
+++ b/app/src/components/layout/sidebarLeft/visualization/widgets/WidgetsTemplate.tsx
@@ -0,0 +1,12 @@
+import React from 'react'
+
+const WidgetsTemplate = () => {
+ return (
+
+ WidgetsTemplate
+
+
+ )
+}
+
+export default WidgetsTemplate
diff --git a/app/src/components/layout/sidebarRight/SideBarRight.tsx b/app/src/components/layout/sidebarRight/SideBarRight.tsx
index 9cac07b..db44f69 100644
--- a/app/src/components/layout/sidebarRight/SideBarRight.tsx
+++ b/app/src/components/layout/sidebarRight/SideBarRight.tsx
@@ -8,6 +8,7 @@ import {
} from "../../icons/SimulationIcons";
import useToggleStore from "../../../store/useUIToggleStore";
import MachineMechanics from "./mechanics/MachineMechanics";
+import Visualization from "./visualization/Visualization";
const SideBarRight: React.FC = () => {
const { activeModule } = useModuleStore();
@@ -54,13 +55,17 @@ const SideBarRight: React.FC = () => {
)}
)}
- {toggleUI && (
+ {/* process builder */}
+ {toggleUI && activeModule === "builder" && (
)}
+
+ {/* realtime visualization */}
+ {activeModule === "visualization" && }
);
};
diff --git a/app/src/components/layout/sidebarRight/visualization/Visualization.tsx b/app/src/components/layout/sidebarRight/visualization/Visualization.tsx
new file mode 100644
index 0000000..c29aef2
--- /dev/null
+++ b/app/src/components/layout/sidebarRight/visualization/Visualization.tsx
@@ -0,0 +1,28 @@
+import { useState } from "react";
+import Search from "../../../ui/inputs/Search";
+import ToggleHeader from "../../../ui/inputs/ToggleHeader";
+import Data from "./data/Data";
+import Design from "./design/Design";
+
+const Visualization = () => {
+ const [activeOption, setActiveOption] = useState("Data");
+
+ const handleToggleClick = (option: string) => {
+ setActiveOption(option); // Update the active option
+ };
+
+ return (
+
+
+
+ {activeOption === "Data" ? : }
+
+
+ );
+};
+
+export default Visualization;
diff --git a/app/src/components/layout/sidebarRight/visualization/data/Data.tsx b/app/src/components/layout/sidebarRight/visualization/data/Data.tsx
new file mode 100644
index 0000000..d1122bb
--- /dev/null
+++ b/app/src/components/layout/sidebarRight/visualization/data/Data.tsx
@@ -0,0 +1,138 @@
+import React, { useEffect, useState } from "react";
+import { useWidgetStore } from "../../../../../store/useWidgetStore";
+import { RemoveIcon } from "../../../../icons/ExportCommonIcons";
+import RegularDropDown from "../../../../ui/inputs/RegularDropDown";
+import MultiLevelDropDown from "../../../../ui/inputs/MultiLevelDropDown";
+
+interface Child {
+ id: number;
+ easing: string;
+}
+const DATA_STRUCTURE = {
+ furnace: {
+ coolingRate: "coolingRate",
+ furnaceTemperature: "furnaceTemperature",
+ heatingRate: "heatingRate",
+ machineId: "machineId",
+ powerConsumption: "powerConsumption",
+ status: "status",
+ timestamp: "timestamp",
+ vacuumLevel: "vacuumLevel",
+ },
+ testDevice: {
+ abrasiveLevel: {
+ data1: "Data 1",
+ data2: "Data 2",
+ data3: "Data 3",
+ },
+ airPressure: "airPressure",
+ machineId: "machineId",
+ powerConsumption: "powerConsumption",
+ status: "status",
+ temperature: {
+ data1: "Data 1",
+ data2: "Data 2",
+ data3: "Data 3",
+ },
+ timestamp: {
+ data1: "Data 1",
+ data2: "Data 2",
+ data3: "Data 3",
+ },
+ },
+};
+
+interface Group {
+ id: number;
+ easing: string;
+ children: Child[];
+}
+
+const Data = () => {
+ const { selectedChartId } = useWidgetStore();
+
+ // State to store groups for all widgets (using Widget.id as keys)
+ const [chartDataGroups, setChartDataGroups] = useState<
+ Record
+ >({});
+
+ useEffect(() => {
+ // Initialize data groups for the newly selected widget if it doesn't exist
+ if (selectedChartId && !chartDataGroups[selectedChartId.id]) {
+ setChartDataGroups((prev) => ({
+ ...prev,
+ [selectedChartId.id]: [
+ {
+ id: Date.now(),
+ easing: "Connecter 1",
+ children: [
+ { id: Date.now(), easing: "Linear" },
+ { id: Date.now() + 1, easing: "Ease Out" },
+ { id: Date.now() + 2, easing: "Linear" },
+ ],
+ },
+ ],
+ }));
+ }
+ }, [selectedChartId]);
+
+ // Handle adding a new child to the group
+ const handleAddClick = (groupId: number) => {
+ setChartDataGroups((prevGroups) => {
+ const currentGroups = prevGroups[selectedChartId.id] || [];
+ const group = currentGroups.find((g) => g.id === groupId);
+
+ if (group && group.children.length < 7) {
+ const newChild = { id: Date.now(), easing: "Linear" };
+ return {
+ ...prevGroups,
+ [selectedChartId.id]: currentGroups.map((g) =>
+ g.id === groupId ? { ...g, children: [...g.children, newChild] } : g
+ ),
+ };
+ }
+ return prevGroups;
+ });
+ };
+
+ // Remove a child from a group
+ const removeChild = (groupId: number, childId: number) => {
+ setChartDataGroups((currentGroups) => {
+ const currentChartData = currentGroups[selectedChartId.id] || [];
+
+ return {
+ ...currentGroups,
+ [selectedChartId.id]: currentChartData.map((group) =>
+ group.id === groupId
+ ? {
+ ...group,
+ children: group.children.filter(
+ (child) => child.id !== childId
+ ),
+ }
+ : group
+ ),
+ };
+ });
+ };
+
+ return (
+
+ {selectedChartId?.title && (
+
{selectedChartId?.title}
+ )}
+ {/*
*/}
+
+
i
+
+
+ By adding templates and widgets, you create a customizable and
+ dynamic environment.
+
+
+
+
+ );
+};
+
+export default Data;
diff --git a/app/src/components/layout/sidebarRight/visualization/design/Design.tsx b/app/src/components/layout/sidebarRight/visualization/design/Design.tsx
new file mode 100644
index 0000000..93332dd
--- /dev/null
+++ b/app/src/components/layout/sidebarRight/visualization/design/Design.tsx
@@ -0,0 +1,199 @@
+import React, { useState } from "react";
+import { useWidgetStore } from "../../../../../store/useWidgetStore";
+import ChartComponent from "../../../sidebarLeft/visualization/widgets/ChartComponent";
+import RegularDropDown from "../../../../ui/inputs/RegularDropDown";
+
+// Define Props Interface
+interface Widget {
+ id: string;
+ type: string; // Chart type (e.g., "bar", "line")
+ panel: "top" | "bottom" | "left" | "right"; // Panel location
+ title: string;
+ fontFamily?: string;
+ fontSize?: string;
+ fontWeight?: string;
+ data: {
+ labels: string[];
+ datasets: {
+ data: number[];
+ backgroundColor: string;
+ borderColor: string;
+ borderWidth: number;
+ }[];
+ }; // Data for the chart
+}
+
+const Design = () => {
+ const [selectedName, setSelectedName] = useState("drop down");
+ const [selectedElement, setSelectedElement] = useState("drop down");
+ const [selectedFont, setSelectedFont] = useState("drop down");
+ const [selectedSize, setSelectedSize] = useState("drop down");
+ const [selectedWeight, setSelectedWeight] = useState("drop down");
+ const [elementColor, setElementColor] = useState("#6f42c1"); // Default color for elements
+ const [showColorPicker, setShowColorPicker] = useState(false); // Manage visibility of the color picker
+
+ // Zustand Store Hooks
+ const { selectedChartId, setSelectedChartId, widgets, setWidgets } =
+ useWidgetStore();
+
+ // Handle Widget Updates
+ const handleUpdateWidget = (updatedProperties: Partial) => {
+ if (!selectedChartId) return;
+
+ // Update the selectedChartId
+ const updatedChartId = {
+ ...selectedChartId,
+ ...updatedProperties,
+ };
+ setSelectedChartId(updatedChartId);
+
+ // Update the widgets array
+ const updatedWidgets = widgets.map((widget) =>
+ widget.id === selectedChartId.id
+ ? { ...widget, ...updatedProperties }
+ : widget
+ );
+ setWidgets(updatedWidgets);
+ };
+
+ // Default Chart Data
+ const defaultChartData = {
+ labels: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul"],
+ datasets: [
+ {
+ data: [65, 59, 80, 81, 56, 55, 40],
+ backgroundColor: elementColor, // Default background color
+ borderColor: "#ffffff", // Default border color
+ borderWidth: 1,
+ },
+ ],
+ };
+
+ return (
+
+ {/* Title of the Selected Widget */}
+
+ {selectedChartId?.title || "Widget 1"}
+
+
+ {/* Chart Component */}
+
+ {selectedChartId && (
+
+ )}
+
+
+ {/* Options Container */}
+
+ {/* Name Dropdown */}
+
+ Name
+ {
+ setSelectedName(value);
+ handleUpdateWidget({ title: value });
+ }}
+ />
+
+
+ {/* Element Dropdown */}
+
+ Element
+ {
+ setSelectedElement(value);
+ handleUpdateWidget({ type: value });
+ }}
+ />
+
+
+ {/* Font Family Dropdown */}
+
+ Font Family
+ {
+ setSelectedFont(value);
+ handleUpdateWidget({ fontFamily: value });
+ }}
+ />
+
+
+ {/* Size Dropdown */}
+
+ Size
+ {
+ setSelectedSize(value);
+ handleUpdateWidget({ fontSize: value });
+ }}
+ />
+
+
+ {/* Weight Dropdown */}
+
+ Weight
+ {
+ setSelectedWeight(value);
+ handleUpdateWidget({ fontWeight: value });
+ }}
+ />
+
+
+ {/* Element Color Picker */}
+
+
setShowColorPicker((prev) => !prev)}
+ >
+
Element Color
+
▾
{" "}
+ {/* Change icon based on the visibility */}
+
+
+ {/* Show color picker only when 'showColorPicker' is true */}
+ {showColorPicker && (
+
+ {
+ setElementColor(e.target.value);
+ handleUpdateWidget({
+ data: {
+ labels: selectedChartId?.data?.labels || [],
+ datasets: [
+ {
+ ...selectedChartId?.data?.datasets[0],
+ backgroundColor: e.target.value, // Update the element color
+ },
+ ],
+ },
+ });
+ }}
+ />
+ {/* Display the selected color value */}
+ {elementColor}
+
+ )}
+
+
+
+ );
+};
+
+export default Design;
diff --git a/app/src/components/ui/ModuleToggle.tsx b/app/src/components/ui/ModuleToggle.tsx
index cbe64e3..bea9c43 100644
--- a/app/src/components/ui/ModuleToggle.tsx
+++ b/app/src/components/ui/ModuleToggle.tsx
@@ -2,6 +2,7 @@ import React from "react";
import useModuleStore from "../../store/useModuleStore";
import {
BuilderIcon,
+ CartIcon,
SimulationIcon,
VisualizationIcon,
} from "../icons/ExportModuleIcons";
@@ -40,6 +41,17 @@ const ModuleToggle: React.FC = () => {
Visualization
+ setActiveModule("market")}
+ >
+
+
+
+
Market Place
+
);
};
diff --git a/app/src/components/ui/componets/AddButtons.tsx b/app/src/components/ui/componets/AddButtons.tsx
new file mode 100644
index 0000000..31110a6
--- /dev/null
+++ b/app/src/components/ui/componets/AddButtons.tsx
@@ -0,0 +1,187 @@
+import React from "react";
+import { CleanPannel, EyeIcon, LockIcon } from "../../icons/RealTimeVisulationIcons";
+
+// Define the type for `Side`
+type Side = "top" | "bottom" | "left" | "right";
+
+// Define the type for the props passed to the Buttons component
+interface ButtonsProps {
+ selectedZone: {
+ zoneName: string; // Add zoneName property
+ activeSides: Side[];
+ panelOrder: Side[];
+ lockedPanels: Side[];
+ widgets: {
+ id: string;
+ type: string;
+ title: string;
+ panel: Side;
+ data: any;
+ }[];
+ };
+ setSelectedZone: React.Dispatch<
+ React.SetStateAction<{
+ zoneName: string; // Ensure zoneName is included in the state type
+ activeSides: Side[];
+ panelOrder: Side[];
+ lockedPanels: Side[];
+ widgets: {
+ id: string;
+ type: string;
+ title: string;
+ panel: Side;
+ data: any;
+ }[];
+ }>
+ >;
+}
+
+const AddButtons: React.FC = ({
+ selectedZone,
+ setSelectedZone,
+}) => {
+ // Function to toggle lock/unlock a panel
+ const toggleLockPanel = (side: Side) => {
+ const newLockedPanels = selectedZone.lockedPanels.includes(side)
+ ? selectedZone.lockedPanels.filter((panel) => panel !== side)
+ : [...selectedZone.lockedPanels, side];
+
+ const updatedZone = {
+ ...selectedZone,
+ lockedPanels: newLockedPanels,
+ };
+
+ // Update the selectedZone state
+ setSelectedZone(updatedZone);
+ };
+
+ // Function to toggle visibility of a panel
+ const toggleVisibility = (side: Side) => {
+ const newActiveSides = selectedZone.activeSides.includes(side)
+ ? selectedZone.activeSides.filter((s) => s !== side)
+ : [...selectedZone.activeSides, side];
+
+ const updatedZone = {
+ ...selectedZone,
+ activeSides: newActiveSides,
+ panelOrder: newActiveSides,
+ };
+
+ // Update the selectedZone state
+ setSelectedZone(updatedZone);
+ };
+
+ // Function to clean all widgets from a panel
+ const cleanPanel = (side: Side) => {
+ const cleanedWidgets = selectedZone.widgets.filter(
+ (widget) => widget.panel !== side
+ );
+
+ const updatedZone = {
+ ...selectedZone,
+ widgets: cleanedWidgets,
+ };
+
+ // Update the selectedZone state
+ setSelectedZone(updatedZone);
+ };
+
+ // Function to handle "+" button click
+ const handlePlusButtonClick = (side: Side) => {
+ if (selectedZone.activeSides.includes(side)) {
+ // If the panel is already active, remove all widgets and close the panel
+ const cleanedWidgets = selectedZone.widgets.filter(
+ (widget) => widget.panel !== side
+ );
+ const newActiveSides = selectedZone.activeSides.filter((s) => s !== side);
+
+ const updatedZone = {
+ ...selectedZone,
+ widgets: cleanedWidgets,
+ activeSides: newActiveSides,
+ panelOrder: newActiveSides,
+ };
+
+ // Update the selectedZone state
+ setSelectedZone(updatedZone);
+ } else {
+ // If the panel is not active, activate it
+ const newActiveSides = [...selectedZone.activeSides, side];
+
+ const updatedZone = {
+ ...selectedZone,
+ activeSides: newActiveSides,
+ panelOrder: newActiveSides,
+ };
+
+ // Update the selectedZone state
+ setSelectedZone(updatedZone);
+ }
+ };
+
+ return (
+
+ {(["top", "right", "bottom", "left"] as Side[]).map((side) => (
+
+ {/* "+" Button */}
+
handlePlusButtonClick(side)}
+ title={
+ selectedZone.activeSides.includes(side)
+ ? `Remove all items and close ${side} panel`
+ : `Activate ${side} panel`
+ }
+ >
+ +
+
+
+ {/* Extra Buttons */}
+
+ {/* Hide Panel */}
+
toggleVisibility(side)}
+ >
+
+
+
+ {/* Clean Panel */}
+
cleanPanel(side)}
+ >
+
+
+
+ {/* Lock/Unlock Panel */}
+
toggleLockPanel(side)}
+ >
+
+
+
+
+ ))}
+
+ );
+};
+
+export default AddButtons;
diff --git a/app/src/components/ui/componets/DraggableWidget.tsx b/app/src/components/ui/componets/DraggableWidget.tsx
new file mode 100644
index 0000000..342f31f
--- /dev/null
+++ b/app/src/components/ui/componets/DraggableWidget.tsx
@@ -0,0 +1,217 @@
+import { useMemo, useState } from "react";
+import ChartComponent from "../../layout/sidebarLeft/visualization/widgets/ChartComponent";
+import { useWidgetStore } from "../../../store/useWidgetStore";
+
+export const DraggableWidget = ({ widget }: { widget: any }) => {
+ const { selectedChartId, setSelectedChartId } = useWidgetStore();
+
+ // State for managing the popup visibility and customization options
+ const [isPopupOpen, setIsPopupOpen] = useState(false);
+ const [customizationOptions, setCustomizationOptions] = useState({
+ templateBackground: "#ffffff",
+ cardBackground: "#ffffff",
+ cardOpacity: 1,
+ cardBlur: 0,
+ font: "Arial",
+ margin: 0,
+ radius: 5,
+ shadow: "Low",
+ });
+
+ // Handle pointer down to select the chart
+ const handlePointerDown = () => {
+ if (selectedChartId?.id !== widget.id) {
+ setSelectedChartId(widget);
+ }
+ };
+
+ // Handle double-click to open the popup
+ const handleDoubleClick = () => {
+ setIsPopupOpen(true);
+ };
+
+ // Close the popup
+ const handleClosePopup = () => {
+ setIsPopupOpen(false);
+ };
+
+ // Save the changes made in the popup
+ const handleSaveChanges = () => {
+ // Here you can save the customizationOptions to your store or API
+ console.log("Saved Customization Options:", customizationOptions);
+ setIsPopupOpen(false);
+ };
+
+ // Compute dynamic card styles based on customizationOptions
+ const cardStyle = useMemo(() => {
+ const shadowLevels = {
+ Low: "0px 2px 4px rgba(0, 0, 0, 0.1)",
+ Medium: "0px 4px 8px rgba(0, 0, 0, 0.2)",
+ High: "0px 8px 16px rgba(0, 0, 0, 0.3)",
+ };
+
+ return {
+ backgroundColor: customizationOptions.cardBackground,
+ opacity: customizationOptions.cardOpacity,
+ filter: `blur(${customizationOptions.cardBlur}px)`,
+ fontFamily: customizationOptions.font,
+ margin: `${customizationOptions.margin}px`,
+ borderRadius: `${customizationOptions.radius}px`,
+ // boxShadow: shadowLevels[customizationOptions.shadow],
+ };
+ }, [customizationOptions]);
+
+ return (
+ <>
+
+ {widget.type === "progress" ? (
+ //
+ <>>
+ ) : (
+
+ )}
+
+
+ {/* Popup for Customizing Template Theme */}
+ {isPopupOpen && (
+
+
+
Customize Template Theme
+
+ Template Background
+
+ setCustomizationOptions((prev) => ({
+ ...prev,
+ templateBackground: e.target.value,
+ }))
+ }
+ />
+
+
+ Card Background
+
+ setCustomizationOptions((prev) => ({
+ ...prev,
+ cardBackground: e.target.value,
+ }))
+ }
+ />
+
+
+ Card Opacity
+
+ setCustomizationOptions((prev) => ({
+ ...prev,
+ cardOpacity: parseFloat(e.target.value),
+ }))
+ }
+ />
+
+
+ Card Blur
+
+ setCustomizationOptions((prev) => ({
+ ...prev,
+ cardBlur: parseInt(e.target.value),
+ }))
+ }
+ />
+
+
+ Font
+
+ setCustomizationOptions((prev) => ({
+ ...prev,
+ font: e.target.value,
+ }))
+ }
+ >
+ Arial
+ Times New Roman
+ Courier New
+
+
+
+ Margin
+
+ setCustomizationOptions((prev) => ({
+ ...prev,
+ margin: parseInt(e.target.value),
+ }))
+ }
+ />
+
+
+ Radius
+
+ setCustomizationOptions((prev) => ({
+ ...prev,
+ radius: parseInt(e.target.value),
+ }))
+ }
+ />
+
+
+ Shadow
+
+ setCustomizationOptions((prev) => ({
+ ...prev,
+ shadow: e.target.value,
+ }))
+ }
+ >
+ Low
+ Medium
+ High
+
+
+
+ Cancel
+ Save Changes
+
+
+
+ )}
+ >
+ );
+};
diff --git a/app/src/components/ui/componets/Panel.tsx b/app/src/components/ui/componets/Panel.tsx
new file mode 100644
index 0000000..502114b
--- /dev/null
+++ b/app/src/components/ui/componets/Panel.tsx
@@ -0,0 +1,197 @@
+import React, { useEffect, useMemo, useRef, useState } from "react";
+import { useWidgetStore } from "../../../store/useWidgetStore";
+import { usePlayButtonStore } from "../../../store/usePlayButtonStore";
+import { DraggableWidget } from "./DraggableWidget";
+
+type Side = "top" | "bottom" | "left" | "right";
+
+interface Widget {
+ id: string;
+ type: string;
+ title: string;
+ panel: Side;
+ data: any;
+}
+
+interface PanelProps {
+ selectedZone: {
+ zoneName: string;
+ activeSides: Side[];
+ panelOrder: Side[];
+ lockedPanels: Side[];
+ widgets: Widget[];
+ };
+ setSelectedZone: React.Dispatch<
+ React.SetStateAction<{
+ zoneName: string;
+ activeSides: Side[];
+ panelOrder: Side[];
+ lockedPanels: Side[];
+ widgets: Widget[];
+ }>
+ >;
+}
+
+const generateUniqueId = () =>
+ `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
+
+const Panel: React.FC = ({ selectedZone, setSelectedZone }) => {
+ const panelRefs = useRef<{ [side in Side]?: HTMLDivElement }>({});
+ const [panelDimensions, setPanelDimensions] = useState<{
+ [side in Side]?: { width: number; height: number };
+ }>({});
+
+ const getPanelStyle = useMemo(
+ () => (side: Side) => {
+ const currentIndex = selectedZone.panelOrder.indexOf(side);
+ const previousPanels = selectedZone.panelOrder.slice(0, currentIndex);
+ const leftActive = previousPanels.includes("left");
+ const rightActive = previousPanels.includes("right");
+ const topActive = previousPanels.includes("top");
+ const bottomActive = previousPanels.includes("bottom");
+
+ switch (side) {
+ case "top":
+ case "bottom":
+ return {
+ width: `calc(100% - ${
+ (leftActive ? 204 : 0) + (rightActive ? 204 : 0)
+ }px)`,
+ left: leftActive ? "204px" : "0",
+ right: rightActive ? "204px" : "0",
+ [side]: "0",
+ height: "200px",
+ };
+ case "left":
+ case "right":
+ return {
+ height: `calc(100% - ${
+ (topActive ? 204 : 0) + (bottomActive ? 204 : 0)
+ }px)`,
+ top: topActive ? "204px" : "0",
+ bottom: bottomActive ? "204px" : "0",
+ [side]: "0",
+ width: "200px",
+ };
+ default:
+ return {};
+ }
+ },
+ [selectedZone.panelOrder]
+ );
+
+ const handleDrop = (e: React.DragEvent, panel: Side) => {
+ e.preventDefault();
+ const { draggedAsset } = useWidgetStore.getState();
+
+ if (draggedAsset) {
+ if (selectedZone.lockedPanels.includes(panel)) return;
+
+ const currentWidgetsInPanel = selectedZone.widgets.filter(
+ (w) => w.panel === panel
+ ).length;
+
+ const dimensions = panelDimensions[panel];
+ const CHART_WIDTH = 200;
+ const CHART_HEIGHT = 200;
+ let maxCharts = 0;
+
+ if (dimensions) {
+ if (panel === "top" || panel === "bottom") {
+ maxCharts = Math.floor(dimensions.width / CHART_WIDTH);
+ } else {
+ maxCharts = Math.floor(dimensions.height / CHART_HEIGHT);
+ }
+ } else {
+ maxCharts = panel === "top" || panel === "bottom" ? 5 : 3;
+ }
+
+ if (currentWidgetsInPanel >= maxCharts) {
+ return;
+ }
+
+ const updatedZone = {
+ ...selectedZone,
+ widgets: [
+ ...selectedZone.widgets,
+ {
+ ...draggedAsset,
+ id: generateUniqueId(),
+ panel,
+ },
+ ],
+ };
+
+ setSelectedZone(updatedZone);
+ }
+ };
+
+ useEffect(() => {
+ const observers: ResizeObserver[] = [];
+ const currentPanelRefs = panelRefs.current;
+
+ selectedZone.activeSides.forEach((side) => {
+ const element = currentPanelRefs[side];
+ if (element) {
+ const observer = new ResizeObserver((entries) => {
+ for (const entry of entries) {
+ const { width, height } = entry.contentRect;
+ setPanelDimensions((prev) => ({
+ ...prev,
+ [side]: { width, height },
+ }));
+ }
+ });
+ observer.observe(element);
+ observers.push(observer);
+ }
+ });
+
+ return () => {
+ observers.forEach((observer) => observer.disconnect());
+ };
+ }, [selectedZone.activeSides]);
+
+ const { isPlaying } = usePlayButtonStore();
+
+ return (
+ <>
+ {selectedZone.activeSides.map((side) => (
+ handleDrop(e, side)}
+ onDragOver={(e) => e.preventDefault()}
+ ref={(el) => {
+ if (el) {
+ panelRefs.current[side] = el;
+ } else {
+ delete panelRefs.current[side];
+ }
+ }}
+ >
+
+ {selectedZone.widgets
+ .filter((w) => w.panel === side)
+ .map((widget) => (
+
+ ))}
+
+
+ ))}
+ >
+ );
+};
+
+export default Panel;
+// only load selected template
+
diff --git a/app/src/components/ui/componets/RealTimeVisulization.tsx b/app/src/components/ui/componets/RealTimeVisulization.tsx
new file mode 100644
index 0000000..5bb2687
--- /dev/null
+++ b/app/src/components/ui/componets/RealTimeVisulization.tsx
@@ -0,0 +1,207 @@
+import React, { useEffect, useState, useRef } from "react";
+import { usePlayButtonStore } from "../../../store/usePlayButtonStore";
+import Panel from "./Panel";
+import AddButtons from "./AddButtons";
+import {
+ CommentIcon,
+ PlayIcon,
+ SaveTeemplateIcon,
+} from "../../icons/RealTimeVisulationIcons";
+import useTemplateStore from "../../../store/useTemplateStore";
+import { useSelectedZoneStore } from "../../../store/useZoneStore";
+
+type Side = "top" | "bottom" | "left" | "right";
+
+interface Widget {
+ id: string;
+ type: string;
+ title: string;
+ panel: Side;
+ data: any;
+}
+
+const RealTimeVisulization: React.FC = () => {
+ const containerRef = useRef(null);
+ const [zonesData, setZonesData] = useState<{
+ [key: string]: {
+ activeSides: Side[];
+ panelOrder: Side[];
+ lockedPanels: Side[];
+ widgets: Widget[];
+ };
+ }>({
+ "Manufacturing unit": {
+ activeSides: [],
+ panelOrder: [],
+ lockedPanels: [],
+ widgets: [],
+ },
+ "Assembly unit": {
+ activeSides: [],
+ panelOrder: [],
+ lockedPanels: [],
+ widgets: [],
+ },
+ "Packing unit": {
+ activeSides: [],
+ panelOrder: [],
+ lockedPanels: [],
+ widgets: [],
+ },
+ Warehouse: {
+ activeSides: [],
+ panelOrder: [],
+ lockedPanels: [],
+ widgets: [],
+ },
+ Inventory: {
+ activeSides: [],
+ panelOrder: [],
+ lockedPanels: [],
+ widgets: [],
+ },
+ });
+
+ const { isPlaying, setIsPlaying } = usePlayButtonStore();
+ const { addTemplate } = useTemplateStore();
+ const { selectedZone, setSelectedZone } = useSelectedZoneStore();
+
+ const generateUniqueId = (): string =>
+ `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
+
+ const captureVisualization = async (): Promise => {
+ const container = containerRef.current;
+ if (!container) return null;
+
+ const canvas = document.createElement('canvas');
+ const ctx = canvas.getContext('2d');
+ if (!ctx) return null;
+
+ const rect = container.getBoundingClientRect();
+ canvas.width = rect.width;
+ canvas.height = rect.height;
+
+ // Draw background
+ ctx.fillStyle = getComputedStyle(container).backgroundColor;
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
+
+ // Capture all canvas elements
+ const canvases = container.querySelectorAll('canvas');
+ canvases.forEach(childCanvas => {
+ const childRect = childCanvas.getBoundingClientRect();
+ const x = childRect.left - rect.left;
+ const y = childRect.top - rect.top;
+ ctx.drawImage(childCanvas, x, y, childRect.width, childRect.height);
+ });
+
+ // Capture SVG elements
+ const svgs = container.querySelectorAll('svg');
+ for (const svg of Array.from(svgs)) {
+ const svgString = new XMLSerializer().serializeToString(svg);
+ const svgBlob = new Blob([svgString], { type: 'image/svg+xml' });
+ const url = URL.createObjectURL(svgBlob);
+
+ try {
+ const img = await new Promise((resolve, reject) => {
+ const image = new Image();
+ image.onload = () => resolve(image);
+ image.onerror = reject;
+ image.src = url;
+ });
+
+ const svgRect = svg.getBoundingClientRect();
+ ctx.drawImage(
+ img,
+ svgRect.left - rect.left,
+ svgRect.top - rect.top,
+ svgRect.width,
+ svgRect.height
+ );
+ } finally {
+ URL.revokeObjectURL(url);
+ }
+ }
+
+ return canvas.toDataURL('image/png');
+ };
+
+ const handleSaveTemplate = async () => {
+ const snapshot = await captureVisualization();
+ const template = {
+ id: generateUniqueId(),
+ name: `Template ${Date.now()}`,
+ panelOrder: selectedZone.panelOrder,
+ widgets: selectedZone.widgets,
+ snapshot,
+ };
+ addTemplate(template);
+
+ };
+
+ useEffect(() => {
+ setZonesData((prev) => ({
+ ...prev,
+ [selectedZone.zoneName]: selectedZone,
+ }));
+ }, [selectedZone]);
+
+ return (
+
+
+
+
+
+
+
+
+
setIsPlaying(!isPlaying)}
+ >
+
+
+
+
+
+ {Object.keys(zonesData).map((zoneName, index) => (
+
{
+ setSelectedZone({
+ zoneName,
+ ...zonesData[zoneName],
+ });
+ }}
+ >
+ {zoneName}
+
+ ))}
+
+
+ {!isPlaying && (
+
+ )}
+
+
+
+ );
+};
+
+export default RealTimeVisulization;
\ No newline at end of file
diff --git a/app/src/components/ui/inputs/MultiLevelDropDown.tsx b/app/src/components/ui/inputs/MultiLevelDropDown.tsx
new file mode 100644
index 0000000..215bbd8
--- /dev/null
+++ b/app/src/components/ui/inputs/MultiLevelDropDown.tsx
@@ -0,0 +1,94 @@
+import React, { useState } from "react";
+
+
+// Dropdown Item Component
+const DropdownItem = ({
+ label,
+ href,
+ onClick,
+}: {
+ label: string;
+ href?: string;
+ onClick?: () => void;
+}) => (
+ {
+ e.preventDefault();
+ onClick?.();
+ }}
+ >
+ {label}
+
+);
+
+// Nested Dropdown Component
+const NestedDropdown = ({
+ label,
+ children,
+}: {
+ label: string;
+ children: React.ReactNode;
+}) => {
+ const [open, setOpen] = useState(false);
+
+ return (
+
+ {/* Dropdown Trigger */}
+
setOpen(!open)} // Toggle submenu on click
+ >
+ {label} {open ? "▼" : "▶"}
+
+
+ {/* Submenu */}
+ {open &&
{children}
}
+
+ );
+};
+
+// Recursive Function to Render Nested Data
+const renderNestedData = (data: Record) => {
+ return Object.entries(data).map(([key, value]) => {
+ if (typeof value === "object" && !Array.isArray(value)) {
+ // If the value is an object, render it as a nested dropdown
+ return (
+
+ {renderNestedData(value)}
+
+ );
+ } else if (Array.isArray(value)) {
+ // If the value is an array, render each item as a dropdown item
+ return value.map((item, index) => (
+
+ ));
+ } else {
+ // If the value is a simple string, render it as a dropdown item
+ return ;
+ }
+ });
+};
+
+// Main Multi-Level Dropdown Component
+const MultiLevelDropdown = ({ data }: { data: Record }) => {
+ const [open, setOpen] = useState(false);
+
+ return (
+
+ {/* Dropdown Trigger Button */}
+
setOpen(!open)} // Toggle main menu on click
+ >
+ Dropdown trigger ▼
+
+
+ {/* Dropdown Menu */}
+ {open &&
{renderNestedData(data)}
}
+
+ );
+};
+
+export default MultiLevelDropdown;
\ No newline at end of file
diff --git a/app/src/components/ui/inputs/RegularDropDown.tsx b/app/src/components/ui/inputs/RegularDropDown.tsx
new file mode 100644
index 0000000..318913f
--- /dev/null
+++ b/app/src/components/ui/inputs/RegularDropDown.tsx
@@ -0,0 +1,82 @@
+import React, { useState, useEffect, useRef } from "react";
+
+interface DropdownProps {
+ header: string;
+ options: string[];
+ onSelect: (option: string) => void;
+}
+
+const RegularDropDown: React.FC = ({
+ header,
+ options,
+ onSelect,
+}) => {
+ const [isOpen, setIsOpen] = useState(false);
+ const [selectedOption, setSelectedOption] = useState(null);
+ const dropdownRef = useRef(null); // Create a ref for the dropdown container
+
+ // Reset selectedOption when the dropdown closes
+ useEffect(() => {
+ if (!isOpen) {
+ setSelectedOption(null); // Clear local state when the dropdown closes
+ }
+ }, [isOpen]);
+
+ // Reset selectedOption when the header prop changes
+ useEffect(() => {
+ setSelectedOption(null); // Ensure the dropdown reflects the updated header
+ }, [header]);
+
+ // Close dropdown if clicked outside
+ useEffect(() => {
+ const handleClickOutside = (event: MouseEvent) => {
+ if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
+ setIsOpen(false); // Close the dropdown if clicked outside
+ }
+ };
+
+ document.addEventListener("click", handleClickOutside);
+
+ // Cleanup the event listener on component unmount
+ return () => {
+ document.removeEventListener("click", handleClickOutside);
+ };
+ }, []);
+
+ const toggleDropdown = () => {
+ setIsOpen((prev) => !prev);
+ };
+
+ const handleOptionClick = (option: string) => {
+ setSelectedOption(option);
+ onSelect(option); // Call the onSelect function passed from the parent
+ setIsOpen(false); // Close the dropdown after selection
+ };
+
+ return (
+
+ {/* Dropdown Header */}
+
+
{selectedOption || header}
+
▾
+
+
+ {/* Dropdown Options */}
+ {isOpen && (
+
+ {options.map((option, index) => (
+
handleOptionClick(option)}
+ >
+ {option}
+
+ ))}
+
+ )}
+
+ );
+};
+
+export default RegularDropDown;
diff --git a/app/src/pages/Project.tsx b/app/src/pages/Project.tsx
index 6e0b37b..29f8c6c 100644
--- a/app/src/pages/Project.tsx
+++ b/app/src/pages/Project.tsx
@@ -1,14 +1,19 @@
-import React from 'react';
-import ModuleToggle from '../components/ui/ModuleToggle';
-import SideBarLeft from '../components/layout/sidebarLeft/SideBarLeft';
-import SideBarRight from '../components/layout/sidebarRight/SideBarRight';
+import React from "react";
+import ModuleToggle from "../components/ui/ModuleToggle";
+import SideBarLeft from "../components/layout/sidebarLeft/SideBarLeft";
+import SideBarRight from "../components/layout/sidebarRight/SideBarRight";
+import useModuleStore from "../store/useModuleStore";
+import RealTimeVisulization from "../components/ui/componets/RealTimeVisulization";
const Project: React.FC = () => {
+ const { activeModule } = useModuleStore();
+
return (
+ {activeModule === "visualization" && }
);
};
diff --git a/app/src/store/useModuleStore.ts b/app/src/store/useModuleStore.ts
index 5281c0b..6d26a8e 100644
--- a/app/src/store/useModuleStore.ts
+++ b/app/src/store/useModuleStore.ts
@@ -1,5 +1,5 @@
// store/useModuleStore.ts
-import { create } from 'zustand';
+import { create } from "zustand";
interface ModuleStore {
activeModule: string;
@@ -7,7 +7,7 @@ interface ModuleStore {
}
const useModuleStore = create((set) => ({
- activeModule: 'builder', // Initial state
+ activeModule: "visualization", // Initial state
setActiveModule: (module) => set({ activeModule: module }), // Update state
}));
diff --git a/app/src/store/usePlayButtonStore.ts b/app/src/store/usePlayButtonStore.ts
new file mode 100644
index 0000000..5b2913c
--- /dev/null
+++ b/app/src/store/usePlayButtonStore.ts
@@ -0,0 +1,11 @@
+import { create } from "zustand";
+
+type PlayButtonStore = {
+ isPlaying: boolean; // Updated state name to reflect the play/pause status more clearly
+ setIsPlaying: (value: boolean) => void; // Updated setter function name for clarity
+};
+
+export const usePlayButtonStore = create((set) => ({
+ isPlaying: false, // Default state for play/pause
+ setIsPlaying: (value) => set({ isPlaying: value }), // Update isPlaying state
+}));
diff --git a/app/src/store/useTemplateStore.ts b/app/src/store/useTemplateStore.ts
new file mode 100644
index 0000000..a91c14a
--- /dev/null
+++ b/app/src/store/useTemplateStore.ts
@@ -0,0 +1,39 @@
+import { create } from "zustand";
+
+type Side = "top" | "bottom" | "left" | "right";
+
+interface Widget {
+ id: string;
+ type: string;
+ title: string;
+ panel: Side;
+ data: any;
+}
+
+interface Template {
+ id: string;
+ name: string;
+ panelOrder: Side[];
+ widgets: Widget[];
+ snapshot?: string; // Add an optional image property (base64)
+}
+
+interface TemplateStore {
+ templates: Template[];
+ addTemplate: (template: Template) => void;
+ removeTemplate: (id: string) => void;
+}
+
+export const useTemplateStore = create((set) => ({
+ templates: [],
+ addTemplate: (template) =>
+ set((state) => ({
+ templates: [...state.templates, template],
+ })),
+ removeTemplate: (id) =>
+ set((state) => ({
+ templates: state.templates.filter((t) => t.id !== id),
+ })),
+}));
+
+export default useTemplateStore;
diff --git a/app/src/store/useThemeStore.ts b/app/src/store/useThemeStore.ts
new file mode 100644
index 0000000..944a77f
--- /dev/null
+++ b/app/src/store/useThemeStore.ts
@@ -0,0 +1,11 @@
+import { create } from "zustand";
+
+interface ThemeState {
+ themeColor: string[]; // This should be an array of strings
+ setThemeColor: (colors: string[]) => void; // This function will accept an array of strings
+}
+
+export const useThemeStore = create((set) => ({
+ themeColor: ["#5c87df", "#EEEEFE", "#969BA7"],
+ setThemeColor: (colors) => set({ themeColor: colors }),
+}));
diff --git a/app/src/store/useWidgetStore.ts b/app/src/store/useWidgetStore.ts
new file mode 100644
index 0000000..013581b
--- /dev/null
+++ b/app/src/store/useWidgetStore.ts
@@ -0,0 +1,49 @@
+import { create } from "zustand";
+
+export interface Widget {
+ id: string;
+ type: string; // Can be chart type or "progress"
+ panel: "top" | "bottom" | "left" | "right";
+ title: string;
+ fontFamily?: string;
+ fontSize?: string;
+ fontWeight?: string;
+ data: {
+ // Chart data
+ labels?: string[];
+ datasets?: Array<{
+ data: number[];
+ backgroundColor: string;
+ borderColor: string;
+ borderWidth: number;
+ }>;
+ // Progress card data
+ stocks?: Array<{
+ key: string;
+ value: number;
+ description: string;
+ }>;
+ };
+}
+
+interface WidgetStore {
+ draggedAsset: Widget | null; // The currently dragged widget asset
+ widgets: Widget[]; // List of all widgets
+ selectedChartId: any;
+ setDraggedAsset: (asset: Widget | null) => void; // Setter for draggedAsset
+ addWidget: (widget: Widget) => void; // Add a new widget
+ setWidgets: (widgets: Widget[]) => void; // Replace the entire widgets array
+ setSelectedChartId: (widget: Widget | null) => void; // Set the selected chart/widget
+}
+
+// Create the store with Zustand
+export const useWidgetStore = create((set) => ({
+ draggedAsset: null,
+ widgets: [],
+ selectedChartId: null, // Initialize as null, not as an array
+ setDraggedAsset: (asset) => set({ draggedAsset: asset }),
+ addWidget: (widget) =>
+ set((state) => ({ widgets: [...state.widgets, widget] })),
+ setWidgets: (widgets) => set({ widgets }),
+ setSelectedChartId: (widget) => set({ selectedChartId: widget }),
+}));
diff --git a/app/src/store/useZoneStore.ts b/app/src/store/useZoneStore.ts
new file mode 100644
index 0000000..5d7c4a4
--- /dev/null
+++ b/app/src/store/useZoneStore.ts
@@ -0,0 +1,41 @@
+import { create } from "zustand";
+
+type Side = "top" | "bottom" | "left" | "right";
+
+interface Widget {
+ id: string;
+ type: string;
+ title: string;
+ panel: Side;
+ data: any;
+}
+
+interface SelectedZoneState {
+ zoneName: string;
+ activeSides: Side[];
+ panelOrder: Side[];
+ lockedPanels: Side[];
+ widgets: Widget[];
+}
+
+interface SelectedZoneStore {
+ selectedZone: SelectedZoneState;
+ setSelectedZone: (zone: Partial | ((prev: SelectedZoneState) => SelectedZoneState)) => void;
+}
+
+export const useSelectedZoneStore = create((set) => ({
+ selectedZone: {
+ zoneName: "Manufacturing unit",
+ activeSides: [],
+ panelOrder: [],
+ lockedPanels: [],
+ widgets: [],
+ },
+ setSelectedZone: (zone) =>
+ set((state) => ({
+ selectedZone:
+ typeof zone === "function"
+ ? zone(state.selectedZone) // Handle functional updates
+ : { ...state.selectedZone, ...zone }, // Handle partial updates
+ })),
+}));
\ No newline at end of file
diff --git a/app/src/styles/components/_regularDropDown.scss b/app/src/styles/components/_regularDropDown.scss
new file mode 100644
index 0000000..085a5cd
--- /dev/null
+++ b/app/src/styles/components/_regularDropDown.scss
@@ -0,0 +1,52 @@
+@use '../abstracts/variables.scss' as *;
+
+
+.regularDropdown-container {
+ width: 104px;
+ height: 22px;
+ border: 1px solid var(--text-color); // Ensure $border-color is defined
+ border-radius: 6px;
+ position: relative;
+ cursor: pointer;
+ padding: 0 6px;
+
+ .dropdown-header {
+ height: 100%;
+ display: flex;
+ justify-content: space-between;
+ cursor: pointer;
+ // padding: 5px;
+ border: 1px solid var(--primary-color);
+ border-radius: 6px;
+ background-color: var(--background-color);
+
+ // .icon {
+ // padding-right: 6px;
+ // }
+ }
+
+ .dropdown-options {
+ position: absolute; // Ensure dropdown options position correctly
+ width: 100%; // Ensure options width matches the header
+ background-color: var(--primary-color); // Optional: Background color
+ border: 1px solid #ccc; // Optional: Border styling
+ border-radius: 4px; // Optional: Border radius
+ z-index: 10; // Ensure dropdown appears above other elements
+ max-height: 200px; // Optional: Limit height
+ overflow-y: auto; // Optional: Enable scrolling if content exceeds height
+ left: 0;
+ top: 104%;
+ .option {
+ padding: 5px;
+ cursor: pointer;
+
+ &:hover {
+ background-color: var(--primary-color); // Optional: Hover effect
+ }
+ }
+ }
+
+ .icon {
+ height: auto;
+ }
+}
\ No newline at end of file
diff --git a/app/src/styles/layout/sidebar.scss b/app/src/styles/layout/sidebar.scss
index 49ce93b..622cade 100644
--- a/app/src/styles/layout/sidebar.scss
+++ b/app/src/styles/layout/sidebar.scss
@@ -9,25 +9,31 @@
background-color: var(--background-color);
border-radius: #{$border-radius-extra-large};
box-shadow: #{$box-shadow-medium};
+
.header-container {
@include flex-space-between;
padding: 10px;
width: 100%;
+
.header-content {
@include flex-center;
width: calc(100% - 34px);
+
.logo-container {
@include flex-center;
}
+
.header-title {
padding: 0 8px;
width: 100%;
max-width: calc(100% - 32px);
+
.input-value {
color: var(--accent-color);
}
}
}
+
.toggle-sidebar-ui-button {
@include flex-center;
cursor: pointer;
@@ -36,31 +42,108 @@
min-height: 32px;
min-width: 32px;
border-radius: #{$border-radius-small};
+
&:hover {
background-color: var(--background-color-secondary);
}
}
+
.active {
background-color: var(--background-color-secondary);
outline: 1px solid var(--accent-color);
outline-offset: -1px;
}
}
+
.sidebar-left-container {
min-height: 50vh;
padding-bottom: 12px;
position: relative;
display: flex;
flex-direction: column;
+
.sidebar-left-content-container {
border-bottom: 1px solid var(--border-color);
// flex: 1;
height: calc(100% - 36px);
position: relative;
overflow: auto;
+
+ .widget-left-sideBar {
+ min-height: 50vh;
+ max-height: 60vh;
+
+ .widget2D {
+ .chart-container {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ padding-right: 6px;
+ flex-wrap: nowrap;
+ overflow: auto;
+
+ .chart {
+ min-height: 170px;
+ background: var(--background-primary, #FCFDFD);
+ border: 1.23px solid var(--Grays-Gray-5, #E5E5EA);
+ box-shadow: 0px 4.91px 4.91px 0px #0000001C;
+ border-radius: $border-radius-medium;
+ padding: 12px 6px;
+ }
+
+ .progressBar {
+ height: auto !important;
+ padding: 12px 10px 41px 10px;
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+
+ .header {
+ display: flex;
+ justify-content: start;
+ align-items: center;
+ border-bottom: none;
+ }
+
+ .stock {
+ padding: 13px 5px;
+ background-color: #E0DFFF80;
+ border-radius: 6.33px;
+ display: flex;
+ justify-content: space-between;
+
+ .stock-item {
+ .stockValues {
+ display: flex;
+ flex-direction: row-reverse;
+ align-items: flex-end;
+ gap: 3px;
+
+ .value {
+ color: var(--accent-color);
+ font-size: 16px;
+ }
+ }
+
+ .stock-description {
+ font-size: 12px;
+ }
+ }
+
+ }
+ }
+
+ }
+ }
+ }
+
+
+
}
+
.outline-container {
height: 100%;
+
.outline-content-container {
position: relative;
height: 100%;
@@ -79,15 +162,18 @@
background-color: var(--background-color);
border-radius: #{$border-radius-extra-large};
box-shadow: #{$box-shadow-medium};
+
.header-container {
@include flex-space-between;
padding: 10px;
width: 100%;
gap: 12px;
height: 52px;
+
.options-container {
@include flex-center;
gap: 8px;
+
.share-button {
padding: 4px 12px;
color: var(--primary-color);
@@ -96,18 +182,22 @@
border-radius: #{$border-radius-medium};
cursor: pointer;
}
+
.app-docker-button {
@include flex-center;
}
}
+
.split {
height: 20px;
width: 2px;
background: var(--background-color-secondary);
}
+
.users-container {
width: 100%;
@include flex-space-between;
+
.user-profile {
@include flex-center;
height: 26px;
@@ -118,8 +208,10 @@
font-weight: var(--font-weight-bold);
color: white;
}
+
.guest-users-container {
display: flex;
+
.other-guest {
@include flex-center;
height: 26px;
@@ -134,11 +226,14 @@
outline-offset: -1px;
}
}
+
.user-profile-container {
display: flex;
+
.user-organnization {
height: 100%;
max-width: 52px;
+
img {
height: 100%;
width: 100%;
@@ -148,9 +243,11 @@
}
}
}
+
.sidebar-actions-container {
position: absolute;
left: -40px;
+
.sidebar-action-list {
margin-bottom: 12px;
@include flex-center;
@@ -160,16 +257,19 @@
background: var(--primary-color);
box-shadow: #{$box-shadow-medium};
}
+
.active {
background: var(--accent-color);
}
}
+
.sidebar-right-container {
min-height: 50vh;
padding-bottom: 12px;
position: relative;
display: flex;
flex-direction: column;
+
.sidebar-right-content-container {
border-bottom: 1px solid var(--border-color);
// flex: 1;
@@ -178,12 +278,179 @@
overflow: auto;
}
}
+
+ .visualization-right-sideBar {
+ min-height: 50vh;
+ max-height: 60vh;
+
+ .sidebar-left-content-container {
+ .dataSideBar {
+ width: 100%;
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+ padding: 10px 12px;
+
+ .sideBarHeader {
+ color: #5c87df;
+ border-bottom: 1px solid var(--border-color);
+ padding-bottom: 6px;
+ }
+
+ .selectedMain-container {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+ border-bottom: 1px solid var(--border-color);
+ padding-bottom: 10px;
+
+ .selectedMain {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+
+ main {
+ width: 35%;
+ white-space: nowrap;
+ /* Prevent wrapping */
+ }
+
+ .icon {
+ padding: 0;
+ cursor: pointer;
+ }
+
+ button {
+ background-color: transparent;
+ box-shadow: none;
+ color: #5273eb;
+ padding: 6px;
+ font-size: 18px;
+ }
+
+ .bulletPoint {
+ color: #5273eb;
+ font-size: 16px;
+ }
+
+ .regularDropdown-container {
+ width: 100%;
+ }
+
+ &:first-child {
+ gap: 4px;
+ }
+ }
+ }
+
+ .child {
+ width: 100%;
+ gap: 6px;
+ }
+
+ .infoBox {
+ display: flex;
+ align-items: flex-start;
+ gap: 6px;
+ color: #444;
+ border-radius: 6px;
+ font-size: 14px;
+
+ .infoIcon {
+ padding: 0px 7px;
+ border-radius: 50%;
+ border: 1px solid gray;
+ }
+
+ p {
+ margin: 0;
+ }
+ }
+ }
+
+ .design {
+ width: 100%;
+ display: flex;
+ flex-direction: column;
+ gap: 15px;
+ padding: 0;
+ font-size: 14px;
+ color: #4a4a4a;
+
+ .selectedWidget {
+ padding: 6px 12px;
+ border-top: 1px solid var(--border-color);
+ border-bottom: 1px solid var(--border-color);
+ }
+ }
+
+ .reviewChart {
+ width: 100%;
+ height: 150px;
+ background: #f0f0f0;
+ border-radius: 8px;
+ }
+
+ .optionsContainer {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+ padding: 0 12px;
+
+ .option {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+
+ .regularDropdown-container {
+ width: 160px;
+ }
+
+ &:last-child {
+ flex-direction: column;
+
+ .header {
+ width: 100%;
+ display: flex;
+ justify-content: space-between;
+ }
+
+ .colorDisplayer {
+ width: 100%;
+ display: flex;
+ justify-content: start;
+ align-items: center;
+
+ input[type="color"] {
+ border: none;
+ outline: none;
+ background: none;
+ width: 24px;
+ height: 26px;
+ border-radius: 3.2px;
+
+ }
+
+ }
+ }
+
+ span {
+ min-width: 100px;
+ }
+ }
+ }
+
+ }
+ }
}
.machine-mechanics-container {
.header {
@include flex-space-between;
padding: 6px 12px;
+
.add-button {
@include flex-center;
padding: 2px 4px;
@@ -191,19 +458,23 @@
color: var(--primary-color);
border-radius: #{$border-radius-small};
cursor: pointer;
+
path {
stroke: var(--primary-color);
}
}
}
+
.lists-main-container {
margin: 2px 8px;
width: calc(100% - 16px);
background: var(--background-color-secondary);
border-radius: #{$border-radius-small};
+
.list-container {
min-height: 120px;
padding: 4px;
+
.list-item {
@include flex-space-between;
padding: 4px 12px;
@@ -211,15 +482,19 @@
margin: 2px 0;
border-radius: #{$border-radius-small};
}
+
.active {
background: var(--accent-color);
+
.value {
color: var(--primary-color);
}
+
path {
stroke: var(--primary-color);
}
}
+
.remove-button {
@include flex-center;
height: 12px;
@@ -227,18 +502,22 @@
cursor: pointer;
}
}
+
.resize-icon {
@include flex-center;
padding: 4px;
cursor: grab;
+
&:active {
cursor: grabbing;
}
}
}
+
.selected-properties-container {
padding: 12px;
}
+
.footer {
@include flex-center;
justify-content: flex-start;
@@ -247,3 +526,127 @@
font-size: var(--font-size-tiny);
}
}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+/* Base styles */
+.multi-level-dropdown {
+ position: relative;
+ display: inline-block;
+ text-align: left;
+
+ .dropdown-button {
+ background-color: #3b82f6; /* Blue background */
+ color: white;
+ padding: 0.5rem 1rem;
+ font-size: 0.875rem;
+ border: none;
+ border-radius: 0.375rem;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ transition: background-color 0.2s ease;
+
+ &:hover {
+ background-color: #2563eb; /* Darker blue on hover */
+ }
+
+ .icon {
+ margin-left: 0.5rem;
+ }
+ }
+
+ .dropdown-menu {
+ position: absolute;
+ top: calc(100% + 0.5rem); /* Add spacing below the button */
+ left: 0;
+ width: 12rem;
+ background-color: white;
+ border: 1px solid #e5e7eb; /* Light gray border */
+ border-radius: 0.375rem;
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); /* Subtle shadow */
+ z-index: 10;
+ }
+}
+
+/* Dropdown Item */
+.dropdown-item {
+ display: block;
+ padding: 0.5rem 1rem;
+ font-size: 0.875rem;
+ color: #4b5563; /* Gray text */
+ text-decoration: none;
+ cursor: pointer;
+ transition: background-color 0.2s ease;
+
+ &:hover {
+ background-color: #f3f4f6; /* Light gray background on hover */
+ }
+}
+
+/* Nested Dropdown */
+.nested-dropdown {
+ position: relative;
+
+ .dropdown-trigger {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 0.5rem 1rem;
+ font-size: 0.875rem;
+ color: #4b5563; /* Gray text */
+ cursor: pointer;
+ transition: background-color 0.2s ease;
+
+ &:hover {
+ background-color: #f3f4f6; /* Light gray background on hover */
+ }
+
+ .icon {
+ margin-left: 0.5rem;
+ }
+ }
+
+ .submenu {
+ position: absolute;
+ top: 0;
+ left: 100%; /* Position submenu to the right */
+ width: 12rem;
+ background-color: white;
+ border: 1px solid #e5e7eb; /* Light gray border */
+ border-radius: 0.375rem;
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); /* Subtle shadow */
+ z-index: 20;
+ }
+}
\ No newline at end of file
diff --git a/app/src/styles/main.scss b/app/src/styles/main.scss
index 2c9b7df..68688f6 100644
--- a/app/src/styles/main.scss
+++ b/app/src/styles/main.scss
@@ -18,9 +18,11 @@
@use 'components/moduleToggle';
@use 'components/templates';
@use 'components/tools';
+@use 'components/regularDropDown';
// layout
@use 'layout/sidebar';
// pages
@use 'pages/home';
+@use 'pages/realTimeViz';
\ No newline at end of file
diff --git a/app/src/styles/pages/realTimeViz.scss b/app/src/styles/pages/realTimeViz.scss
new file mode 100644
index 0000000..6bf0ae3
--- /dev/null
+++ b/app/src/styles/pages/realTimeViz.scss
@@ -0,0 +1,464 @@
+@use '../abstracts/variables.scss' as *;
+
+// Main Container
+.realTime-viz {
+ background-color: var(--background-color);
+ border-radius: 20px;
+ box-shadow: 0px 4px 8px rgba(60, 60, 67, 0.1019607843);
+ width: 55%;
+ height: 600px;
+ position: absolute;
+ top: 50%;
+ left: 48%;
+ transform: translate(-50%, -50%);
+
+ .icon {
+ display: flex;
+ align-items: center;
+ position: relative;
+
+ &:first-child {
+ &::after {
+ content: "";
+ display: block;
+ width: 1px;
+ height: 18px;
+ background-color: #000;
+ margin-top: 10px;
+ position: absolute;
+ right: -10px;
+ top: -4px;
+ }
+ }
+ }
+
+ .icons-container {
+ .icon {
+ &:first-child {
+ &::after {
+ display: none;
+ }
+ }
+ }
+
+
+
+ }
+
+ .realTimeViz-tools {
+ position: fixed;
+ bottom: -150px;
+ left: 50%;
+ transform: translateX(-50%);
+ box-shadow: 0px 4px 8px 0px rgba(60, 60, 67, 0.1);
+ background: $background-color;
+ display: flex;
+ gap: 12px;
+ border-radius: 12px;
+ z-index: 1000;
+ transition: bottom 0.3s ease;
+ padding: 8px;
+
+ .icons-container {
+ padding-left: 8px;
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ }
+ }
+
+
+ .zoon-wrapper {
+ display: flex;
+ background-color: #E0DFFF80;
+ position: absolute;
+ bottom: 10px;
+ left: 50%;
+ transform: translate(-50%, 0);
+ gap: 6px;
+ padding: 4px;
+ border-radius: 8px;
+ max-width: 80%;
+ overflow: auto;
+
+ &::-webkit-scrollbar {
+ display: none;
+ }
+
+ .zone {
+ width: auto;
+ background-color: #FCFDFD;
+ border-radius: 6px;
+ padding: 4px 8px;
+ white-space: nowrap;
+ font-size: $small;
+
+ }
+
+ .active {
+ background-color: var(--accent-color);
+ color: var(--background-color);
+ // color: #FCFDFD !important;
+ }
+ }
+
+ .zoon-wrapper.bottom {
+ bottom: 210px;
+ }
+
+ @media (max-width: 1024px) {
+ width: 80%; // Increase width to take more space on smaller screens
+ height: 500px; // Reduce height to fit smaller screens
+ left: 50%; // Center horizontally
+
+ .main-container {
+ margin: 0 15px; // Reduce margin for better spacing
+ }
+
+ .zoon-wrapper {
+ bottom: 5px; // Adjust position for smaller screens
+
+ &.bottom {
+ bottom: 150px; // Adjust for bottom placement
+ }
+ }
+ }
+
+ @media (max-width: 768px) {
+ width: 90%; // Take even more width on very small screens
+ height: 400px; // Further reduce height
+ top: 45%; // Adjust vertical position slightly upward
+
+ .panel {
+
+ &.top-panel,
+ &.bottom-panel {
+ .panel-content {
+ flex-direction: column; // Stack panels vertically on small screens
+
+ .chart-container {
+ width: 100%; // Make charts full width
+ height: 150px; // Reduce chart height
+ }
+ }
+ }
+ }
+ }
+
+ @media (max-width: 480px) {
+ width: 95%; // Take almost full width on very small devices
+ height: 350px; // Further reduce height
+ top: 40%; // Move slightly higher for better visibility
+
+ .side-button-container {
+ flex-direction: row !important; // Force buttons into a row
+ gap: 4px; // Reduce spacing between buttons
+
+ &.top,
+ &.bottom {
+ left: 50%; // Center horizontally
+ transform: translateX(-50%);
+ }
+ }
+ }
+
+ .content-container {
+ display: flex;
+ height: 100vh;
+ transition: all 0.3s ease;
+ }
+
+ .main-container {
+ position: relative;
+ flex: 1;
+ height: 600px;
+ background-color: rgb(235, 235, 235);
+ margin: 0 30px;
+ transition: height 0.3s ease, margin 0.3s ease;
+
+
+
+ .zoon-wrapper {
+ display: flex;
+ background-color: rgba(224, 223, 255, 0.5);
+ position: absolute;
+ bottom: 10px;
+ left: 50%;
+ transform: translate(-50%, 0);
+ gap: 6px;
+ padding: 4px;
+ border-radius: 8px;
+ max-width: 80%;
+ overflow: auto;
+ transition: transform 0.3s ease;
+
+ &::-webkit-scrollbar {
+ display: none;
+ }
+
+ .zone {
+ width: auto;
+ background-color: $background-color;
+ border-radius: 6px;
+ padding: 4px 8px;
+ white-space: nowrap;
+ cursor: pointer;
+ transition: background-color 0.3s ease;
+
+ &.active {
+ background-color: var(--primary-color);
+ color: var(--accent-color);
+ }
+ }
+
+ &.bottom {
+ bottom: 210px;
+ }
+ }
+ }
+
+ .panel {
+ position: absolute;
+ background: white;
+ box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
+ transition: all 0.3s ease;
+ border-radius: 6px;
+ overflow: visible !important;
+
+ .panel-content {
+ position: relative;
+ height: 100%;
+ padding: 10px;
+ overflow: auto;
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+
+ &::-webkit-scrollbar {
+ display: none;
+ }
+
+ .chart-container {
+ width: 100%;
+ height: 200px;
+ max-height: 100%;
+ border: 1px dotted #a9a9a9;
+ border-radius: 8px;
+ box-shadow: 0px 2px 6px 0px rgba(60, 60, 67, 0.1);
+ padding: 6px 0;
+ background-color: white;
+ }
+
+ .close-btn {
+ position: absolute;
+ top: 5px;
+ right: 5px;
+ background: none;
+ border: none;
+ cursor: pointer;
+ color: var(--primary-color);
+ }
+ }
+
+ &.top-panel,
+ &.bottom-panel {
+ left: 0;
+ right: 0;
+
+ .panel-content {
+ display: flex;
+ flex-direction: row;
+
+ .chart-container {
+ height: 100%;
+ width: 230px;
+ }
+ }
+ }
+
+ &.top-panel {
+ top: 0;
+ }
+
+ &.bottom-panel {
+ bottom: 0;
+ }
+
+ &.left-panel {
+ left: 0;
+ top: 0;
+ bottom: 0;
+
+ .chart-container {
+ width: 100%;
+ height: 200px;
+ }
+ }
+
+ &.right-panel {
+ right: 0;
+ top: 0;
+ bottom: 0;
+ }
+ }
+}
+
+// Side Buttons
+.side-button-container {
+ position: absolute;
+ display: flex;
+ background-color: $background-color;
+ padding: 5px;
+ border-radius: 8px;
+ transition: transform 0.3s ease;
+
+ .extra-Bs {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+
+ .icon {
+ display: flex;
+ }
+
+ &:hover {
+ cursor: pointer;
+ }
+ }
+
+ .side-button {
+ cursor: pointer;
+ transition: background-color 0.3s ease;
+ width: 18px;
+ height: 18px;
+ display: flex;
+ justify-content: center;
+ // align-items: center;
+ background-color: var(--accent-color);
+ border: none;
+ color: var(--background-color);
+ border-radius: 4px;
+
+ &:hover {
+ // background-color: var(--primary-color);
+ // color: var(--accent-color);
+ }
+ }
+
+ &.top {
+ top: -30px;
+ left: 50%;
+ transform: translateX(-50%);
+ flex-direction: row;
+ gap: 6px;
+ }
+
+ &.right {
+ right: -30px;
+ top: 50%;
+ transform: translateY(-50%);
+ flex-direction: column;
+ gap: 6px;
+ }
+
+ &.bottom {
+ bottom: -30px;
+ left: 50%;
+ transform: translateX(-50%);
+ flex-direction: row;
+ gap: 6px;
+ }
+
+ &.left {
+ left: -30px;
+ top: 50%;
+ transform: translateY(-50%);
+ flex-direction: column;
+ gap: 6px;
+ }
+}
+
+.right.side-button-container {
+
+ .extra-Bs {
+ flex-direction: column;
+ }
+
+}
+
+.left.side-button-container {
+
+ .extra-Bs {
+ flex-direction: column;
+ }
+}
+
+// Theme Container
+.theme-container {
+ width: 250px;
+ padding: 12px;
+ box-shadow: 1px -3px 4px 0px rgba(0, 0, 0, 0.11);
+ border-radius: 8px;
+ background-color: white;
+ position: absolute;
+ top: 20px;
+ right: -100%;
+ transform: translate(-0%, 0);
+
+ h2 {
+ font-size: 12px;
+ margin-bottom: 8px;
+ color: #2B3344;
+ }
+
+ .theme-preset-wrapper {
+ display: flex;
+ gap: 5px;
+ flex-wrap: wrap;
+
+ .theme-preset {
+ display: flex;
+ gap: 2px;
+ margin-bottom: 10px;
+ border: 1px solid $border-color;
+ padding: 5px 10px;
+ border-radius: 4px;
+ transition: border 0.3s ease;
+
+ &.active {
+ border: 1px solid var(--primary-color);
+
+ &::after {
+ content: '';
+ position: absolute;
+ top: 1px;
+ left: 1px;
+ width: 10px;
+ height: 10px;
+ background-color: var(--primary-color);
+ border-radius: 50%;
+ }
+ }
+ }
+ }
+
+ .custom-color {
+ display: flex;
+ justify-content: space-between;
+
+ .color-displayer {
+ display: flex;
+ gap: 5px;
+ align-items: center;
+ border: 1px solid var(--accent-color);
+ border-radius: 4px;
+ padding: 0 5px;
+
+ input {
+ border: none;
+ outline: none;
+ border-radius: 50%;
+ }
+ }
+ }
+}
\ No newline at end of file