Your commit message
This commit is contained in:
19
app/package-lock.json
generated
19
app/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -100,3 +100,34 @@ export function VisualizationIcon({ isActive }: { isActive: boolean }) {
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function CartIcon({ isActive }: { isActive: boolean }) {
|
||||
return (
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M1.33337 2L1.50998 2.05887C2.39001 2.35221 2.83002 2.49888 3.08169 2.84807C3.33337 3.19725 3.33337 3.66106 3.33337 4.58869V6.33333C3.33337 8.21893 3.33337 9.16173 3.91916 9.74753C4.50495 10.3333 5.44775 10.3333 7.33337 10.3333H8.66671M12.6667 10.3333H11.3334"
|
||||
stroke={isActive ? "var(--primary-color)" : "var(--text-color)"}
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
<path
|
||||
d="M5.00005 12C5.55233 12 6.00005 12.4477 6.00005 13C6.00005 13.5523 5.55233 14 5.00005 14C4.44776 14 4.00005 13.5523 4.00005 13C4.00005 12.4477 4.44776 12 5.00005 12Z"
|
||||
stroke={isActive ? "var(--primary-color)" : "var(--text-color)"}
|
||||
/>
|
||||
<path
|
||||
d="M11 12C11.5523 12 12 12.4477 12 13C12 13.5523 11.5523 14 11 14C10.4478 14 10 13.5523 10 13C10 12.4477 10.4478 12 11 12Z"
|
||||
stroke={isActive ? "var(--primary-color)" : "var(--text-color)"}
|
||||
/>
|
||||
<path
|
||||
d="M3.33337 4H5.33337M3.66671 8.66667H10.6812C11.3208 8.66667 11.6406 8.66667 11.8911 8.50153C12.1416 8.33633 12.2676 8.0424 12.5195 7.45453L12.8052 6.78787C13.3449 5.52863 13.6148 4.89902 13.3184 4.44951C13.0219 4 12.337 4 10.967 4H8.00004"
|
||||
stroke={isActive ? "var(--primary-color)" : "var(--text-color)"}
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
162
app/src/components/icons/RealTimeVisulationIcons.tsx
Normal file
162
app/src/components/icons/RealTimeVisulationIcons.tsx
Normal file
@@ -0,0 +1,162 @@
|
||||
export function CleanPannel() {
|
||||
return (
|
||||
<svg
|
||||
width="12"
|
||||
height="12"
|
||||
viewBox="0 0 12 12"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g clip-path="url(#clip0_1782_1158)">
|
||||
<path d="M12 0H0V12H12V0Z" fill="white" fill-opacity="0.01" />
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M5 1.47852H7V3.47853H10.75V5.47853H1.25V3.47853H5V1.47852Z"
|
||||
stroke="#2B3344"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path d="M2 10H10V5.5H2V10Z" stroke="#2B3344" stroke-linejoin="round" />
|
||||
<path
|
||||
d="M4 9.97439V8.47852"
|
||||
stroke="#2B3344"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M6 9.97461V8.47461"
|
||||
stroke="#2B3344"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M8 9.97439V8.47852"
|
||||
stroke="#2B3344"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M3 10H9"
|
||||
stroke="#2B3344"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_1782_1158">
|
||||
<rect width="12" height="12" fill="white" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function EyeIcon() {
|
||||
return (
|
||||
<svg
|
||||
width="14"
|
||||
height="15"
|
||||
viewBox="0 0 14 15"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M8.75047 7.4375C8.75047 8.40402 7.967 9.1875 7.00047 9.1875C6.034 9.1875 5.25049 8.40402 5.25049 7.4375C5.25049 6.47097 6.034 5.6875 7.00047 5.6875C7.967 5.6875 8.75047 6.47097 8.75047 7.4375Z"
|
||||
stroke="#1D1E21"
|
||||
strokeOpacity="0.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M7.00086 3.35419C4.3889 3.35419 2.1779 5.07087 1.43457 7.43752C2.17789 9.80416 4.3889 11.5209 7.00086 11.5209C9.6128 11.5209 11.8238 9.80416 12.5671 7.43752C11.8238 5.07088 9.6128 3.35419 7.00086 3.35419Z"
|
||||
stroke="#1D1E21"
|
||||
strokeOpacity="0.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function LockIcon() {
|
||||
return (
|
||||
<svg
|
||||
width="14"
|
||||
height="15"
|
||||
viewBox="0 0 14 15"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M4.0835 6.28763C4.35849 6.27083 4.69751 6.27083 5.1335 6.27083H8.86683C9.30281 6.27083 9.64185 6.27083 9.91683 6.28763M4.0835 6.28763C3.74031 6.30857 3.49683 6.35571 3.28901 6.46158C2.95973 6.62935 2.69201 6.89704 2.52423 7.22633C2.3335 7.60072 2.3335 8.09072 2.3335 9.07083V9.8875C2.3335 10.8676 2.3335 11.3576 2.52423 11.732C2.69201 12.0613 2.95973 12.329 3.28901 12.4967C3.66336 12.6875 4.1534 12.6875 5.1335 12.6875H8.86683C9.84695 12.6875 10.3369 12.6875 10.7113 12.4967C11.0406 12.329 11.3083 12.0613 11.4761 11.732C11.6668 11.3576 11.6668 10.8676 11.6668 9.8875V9.07083C11.6668 8.09072 11.6668 7.60072 11.4761 7.22633C11.3083 6.89704 11.0406 6.62935 10.7113 6.46158C10.5035 6.35571 10.26 6.30857 9.91683 6.28763M4.0835 6.28763V5.10417C4.0835 3.49334 5.38933 2.1875 7.00016 2.1875C8.61098 2.1875 9.91683 3.49334 9.91683 5.10417V6.28763"
|
||||
stroke="#1D1E21"
|
||||
strokeOpacity="0.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function PlayIcon() {
|
||||
return (
|
||||
<svg
|
||||
width="28"
|
||||
height="28"
|
||||
viewBox="0 0 28 28"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M19.9111 12.5144C21.363 13.3799 21.363 15.6201 19.9111 16.4856L11.1451 21.7109C9.73403 22.552 8 21.4572 8 19.7253V9.27468C8 7.54276 9.73403 6.44801 11.145 7.28911L19.9111 12.5144Z"
|
||||
stroke="#1C274C"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function CommentIcon() {
|
||||
return (
|
||||
<svg
|
||||
width="28"
|
||||
height="28"
|
||||
viewBox="0 0 28 28"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M8.15482 20.3118L7.30127 21.1654H8.50838H14H14.0004C15.6584 21.1641 17.2646 20.5879 18.5455 19.5352C19.8263 18.4824 20.7025 17.0181 21.0248 15.3917C21.3471 13.7654 21.0955 12.0776 20.3129 10.6159C19.5303 9.15428 18.2651 8.00918 16.7329 7.37572C15.2007 6.74227 13.4963 6.65966 11.91 7.14196C10.3238 7.62427 8.95377 8.64165 8.0335 10.0208C7.11322 11.3999 6.69958 13.0554 6.86306 14.7053L6.86513 14.7262L6.86895 14.7469C7.0346 15.6431 7.22308 16.3535 7.53795 17.0282C7.85334 17.704 8.28373 18.3189 8.90409 19.0412L8.91809 19.0575L8.93343 19.0725C8.99514 19.133 9.03091 19.215 9.03333 19.3012C9.03257 19.3438 9.02367 19.3858 9.0071 19.425L9.46767 19.6196L9.0071 19.425C8.98993 19.4656 8.96488 19.5024 8.93338 19.5333L8.93336 19.5333L8.92982 19.5368L8.15482 20.3118Z"
|
||||
stroke="#2B3344"
|
||||
/>
|
||||
<path
|
||||
d="M10.6665 12.332H17.3332"
|
||||
stroke="#2B3344"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
<path
|
||||
d="M10.6665 15.668H17.3332"
|
||||
stroke="#2B3344"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function SaveTeemplateIcon() {
|
||||
return (
|
||||
<svg
|
||||
width="28"
|
||||
height="28"
|
||||
viewBox="0 0 28 28"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M21.5 17.4104V13.2492C21.5 9.67539 21.5 7.88847 20.4017 6.77822C19.3033 5.66797 17.5355 5.66797 14 5.66797C10.4645 5.66797 8.6967 5.66797 7.59835 6.77822C6.5 7.88847 6.5 9.67539 6.5 13.2492V17.4104C6.5 19.9909 6.5 21.2811 7.11176 21.8449C7.40351 22.1138 7.77179 22.2827 8.1641 22.3276C8.98668 22.4217 9.94727 21.5721 11.8685 19.8728C12.7177 19.1217 13.1423 18.7461 13.6336 18.6472C13.8755 18.5985 14.1245 18.5985 14.3664 18.6472C14.8577 18.7461 15.2823 19.1217 16.1315 19.8728C18.0527 21.5721 19.0133 22.4217 19.8359 22.3276C20.2282 22.2827 20.5965 22.1138 20.8882 21.8449C21.5 21.2811 21.5 19.9909 21.5 17.4104Z"
|
||||
stroke="#2B3344"
|
||||
/>
|
||||
<path d="M16.5 9H11.5" stroke="#2B3344" stroke-linecap="round" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
<div className="sidebar-left-wrapper">
|
||||
<Header />
|
||||
@@ -28,11 +38,17 @@ const SideBarLeft: React.FC = () => {
|
||||
{activeModule === "visualization" ? (
|
||||
<>
|
||||
<ToggleHeader
|
||||
options={["Outline", "Widgets", "Templates"]}
|
||||
options={["Widgets", "Templates"]}
|
||||
activeOption={activeOption}
|
||||
handleClick={handleToggleClick}
|
||||
/>
|
||||
<Search onChange={handleSearchChange} />
|
||||
<div className="sidebar-left-content-container">
|
||||
{activeOption === "Widgets" ? <Widgets /> : <Templates />}
|
||||
</div>
|
||||
</>
|
||||
) : activeModule === "market" ? (
|
||||
<></>
|
||||
) : (
|
||||
<>
|
||||
<ToggleHeader
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
import React from "react";
|
||||
import useTemplateStore from "../../../../store/useTemplateStore";
|
||||
import { useSelectedZoneStore } from "../../../../store/useZoneStore";
|
||||
|
||||
const Templates = () => {
|
||||
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 (
|
||||
<div className="template-list" style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fill, minmax(180px, 1fr))',
|
||||
gap: '1rem',
|
||||
padding: '1rem'
|
||||
}}>
|
||||
{templates.map((template) => (
|
||||
<div key={template.id} className="template-item" style={{
|
||||
border: '1px solid #e0e0e0',
|
||||
borderRadius: '8px',
|
||||
padding: '1rem',
|
||||
transition: 'box-shadow 0.3s ease',
|
||||
|
||||
|
||||
}}>
|
||||
{template.snapshot && (
|
||||
<div style={{ position: 'relative', paddingBottom: '56.25%' }}> {/* 16:9 aspect ratio */}
|
||||
<img
|
||||
src={template.snapshot} // Corrected from template.image to template.snapshot
|
||||
alt={`${template.name} preview`}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
objectFit: 'contain',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer',
|
||||
transition: 'transform 0.3s ease',
|
||||
// ':hover': {
|
||||
// transform: 'scale(1.05)'
|
||||
// }
|
||||
}}
|
||||
onClick={() => handleLoadTemplate(template)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginTop: '0.5rem'
|
||||
}}>
|
||||
<div
|
||||
onClick={() => handleLoadTemplate(template)}
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
fontWeight: '500',
|
||||
// ':hover': {
|
||||
// textDecoration: 'underline'
|
||||
// }
|
||||
}}
|
||||
>
|
||||
{template.name}
|
||||
</div>
|
||||
<button
|
||||
onClick={() => 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
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{templates.length === 0 && (
|
||||
<div style={{
|
||||
textAlign: 'center',
|
||||
color: '#666',
|
||||
padding: '2rem',
|
||||
gridColumn: '1 / -1'
|
||||
}}>
|
||||
No saved templates yet. Create one in the visualization view!
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Templates;
|
||||
|
||||
@@ -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<HTMLCanvasElement>(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 <canvas ref={canvasRef} style={{ width: "100%", height: "100%" }} />;
|
||||
};
|
||||
|
||||
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)
|
||||
);
|
||||
});
|
||||
@@ -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 (
|
||||
<div className="widget-left-sideBar">
|
||||
<ToggleHeader
|
||||
options={["2D", "3D", "Templates"]}
|
||||
activeOption={activeOption}
|
||||
handleClick={handleToggleClick}
|
||||
/>
|
||||
{activeOption === "2D" && <Widgets2D />}
|
||||
{activeOption === "3D" && <Widgets3D />}
|
||||
{activeOption === "Templates" && <WidgetsTemplate />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Widgets;
|
||||
@@ -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<WidgetProps> = ({ type, index, title }) => {
|
||||
const { setDraggedAsset } = useWidgetStore((state) => state);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`chart chart-${index + 1}`}
|
||||
draggable
|
||||
onDragStart={() => {
|
||||
setDraggedAsset({
|
||||
type,
|
||||
id: `widget-${index + 1}`,
|
||||
title,
|
||||
panel: "top",
|
||||
data: sampleData,
|
||||
});
|
||||
}}
|
||||
onDragEnd={() => setDraggedAsset(null)}
|
||||
>
|
||||
<ChartComponent type={type} title={title} data={sampleData} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const ProgressBarWidget = ({
|
||||
id,
|
||||
title,
|
||||
data,
|
||||
}: {
|
||||
id: string;
|
||||
title: string;
|
||||
data: any;
|
||||
}) => {
|
||||
const { setDraggedAsset } = useWidgetStore((state) => state);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="chart progressBar"
|
||||
draggable
|
||||
onDragStart={() => {
|
||||
setDraggedAsset({
|
||||
type: "progress",
|
||||
id,
|
||||
title,
|
||||
panel: "top",
|
||||
data,
|
||||
});
|
||||
}}
|
||||
onDragEnd={() => setDraggedAsset(null)}
|
||||
>
|
||||
<div className="header">{title}</div>
|
||||
{data.stocks.map((stock: any, index: number) => (
|
||||
<div className="stock" key={index}>
|
||||
<span className="stock-item">
|
||||
<span className="stockValues">
|
||||
<div className="key">{stock.key}</div>
|
||||
<div className="value">{stock.value}</div>
|
||||
</span>
|
||||
<div className="stock-description">{stock.description}</div>
|
||||
</span>
|
||||
<div className="icon">Icon</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Widgets2D = () => {
|
||||
return (
|
||||
<div className="widget2D">
|
||||
<div className="chart-container">
|
||||
{chartTypes.map((type, index) => {
|
||||
const widgetTitle = `Widget ${index + 1}`;
|
||||
return (
|
||||
<ChartWidget
|
||||
key={index}
|
||||
type={type}
|
||||
index={index}
|
||||
title={widgetTitle}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
<ProgressBarWidget
|
||||
id="widget-7"
|
||||
title="Widget 7"
|
||||
data={{
|
||||
stocks: [
|
||||
{ key: "units", value: 1000, description: "Initial stock" },
|
||||
],
|
||||
}}
|
||||
/>
|
||||
<ProgressBarWidget
|
||||
id="widget-8"
|
||||
title="Widget 8"
|
||||
data={{
|
||||
stocks: [
|
||||
{ key: "units", value: 1000, description: "Initial stock" },
|
||||
{ key: "units", value: 500, description: "Additional stock" },
|
||||
],
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Widgets2D;
|
||||
@@ -0,0 +1,11 @@
|
||||
import React from 'react'
|
||||
|
||||
const Widgets3D = () => {
|
||||
return (
|
||||
<div>
|
||||
Widgets3D
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Widgets3D
|
||||
@@ -0,0 +1,12 @@
|
||||
import React from 'react'
|
||||
|
||||
const WidgetsTemplate = () => {
|
||||
return (
|
||||
<div>
|
||||
WidgetsTemplate
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default WidgetsTemplate
|
||||
@@ -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 = () => {
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{toggleUI && (
|
||||
{/* process builder */}
|
||||
{toggleUI && activeModule === "builder" && (
|
||||
<div className="sidebar-right-container">
|
||||
<div className="sidebar-right-content-container">
|
||||
<MachineMechanics />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* realtime visualization */}
|
||||
{activeModule === "visualization" && <Visualization />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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 (
|
||||
<div className="visualization-right-sideBar">
|
||||
<ToggleHeader
|
||||
options={["Data", "Design"]}
|
||||
activeOption={activeOption}
|
||||
handleClick={handleToggleClick}
|
||||
/>
|
||||
<div className="sidebar-left-content-container">
|
||||
{activeOption === "Data" ? <Data /> : <Design />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Visualization;
|
||||
@@ -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<string, Group[]>
|
||||
>({});
|
||||
|
||||
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 (
|
||||
<div className="dataSideBar">
|
||||
{selectedChartId?.title && (
|
||||
<div className="sideBarHeader">{selectedChartId?.title}</div>
|
||||
)}
|
||||
{/* <MultiLevelDropDown data={DATA_STRUCTURE} /> */}
|
||||
<div className="infoBox">
|
||||
<span className="infoIcon">i</span>
|
||||
<p>
|
||||
<em>
|
||||
By adding templates and widgets, you create a customizable and
|
||||
dynamic environment.
|
||||
</em>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Data;
|
||||
@@ -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<Widget>) => {
|
||||
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 (
|
||||
<div className="design">
|
||||
{/* Title of the Selected Widget */}
|
||||
<div className="selectedWidget">
|
||||
{selectedChartId?.title || "Widget 1"}
|
||||
</div>
|
||||
|
||||
{/* Chart Component */}
|
||||
<div className="reviewChart">
|
||||
{selectedChartId && (
|
||||
<ChartComponent
|
||||
type={selectedChartId.type}
|
||||
title={selectedChartId.title}
|
||||
data={selectedChartId.data || defaultChartData} // Use widget data or default
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Options Container */}
|
||||
<div className="optionsContainer">
|
||||
{/* Name Dropdown */}
|
||||
<div className="option">
|
||||
<span>Name</span>
|
||||
<RegularDropDown
|
||||
header={selectedChartId?.title || "Select Name"}
|
||||
options={["Option 1", "Option 2", "Option 3"]}
|
||||
onSelect={(value) => {
|
||||
setSelectedName(value);
|
||||
handleUpdateWidget({ title: value });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Element Dropdown */}
|
||||
<div className="option">
|
||||
<span>Element</span>
|
||||
<RegularDropDown
|
||||
header={selectedChartId?.type || "Select Element"}
|
||||
options={["bar", "line", "pie", "doughnut", "radar", "polarArea"]} // Valid chart types
|
||||
onSelect={(value) => {
|
||||
setSelectedElement(value);
|
||||
handleUpdateWidget({ type: value });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Font Family Dropdown */}
|
||||
<div className="option">
|
||||
<span>Font Family</span>
|
||||
<RegularDropDown
|
||||
header={selectedChartId?.fontFamily || "Select Font"}
|
||||
options={["Arial", "Roboto", "Sans-serif"]}
|
||||
onSelect={(value) => {
|
||||
setSelectedFont(value);
|
||||
handleUpdateWidget({ fontFamily: value });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Size Dropdown */}
|
||||
<div className="option">
|
||||
<span>Size</span>
|
||||
<RegularDropDown
|
||||
header={selectedChartId?.fontSize || "Select Size"}
|
||||
options={["12px", "14px", "16px", "18px"]}
|
||||
onSelect={(value) => {
|
||||
setSelectedSize(value);
|
||||
handleUpdateWidget({ fontSize: value });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Weight Dropdown */}
|
||||
<div className="option">
|
||||
<span>Weight</span>
|
||||
<RegularDropDown
|
||||
header={selectedChartId?.fontWeight || "Select Weight"}
|
||||
options={["Light", "Regular", "Bold"]}
|
||||
onSelect={(value) => {
|
||||
setSelectedWeight(value);
|
||||
handleUpdateWidget({ fontWeight: value });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Element Color Picker */}
|
||||
<div className="option">
|
||||
<div
|
||||
className="header"
|
||||
onClick={() => setShowColorPicker((prev) => !prev)}
|
||||
>
|
||||
<span>Element Color</span>
|
||||
<div className="icon">▾</div>{" "}
|
||||
{/* Change icon based on the visibility */}
|
||||
</div>
|
||||
|
||||
{/* Show color picker only when 'showColorPicker' is true */}
|
||||
{showColorPicker && (
|
||||
<div className="colorDisplayer">
|
||||
<input
|
||||
type="color"
|
||||
value={elementColor}
|
||||
onChange={(e) => {
|
||||
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 */}
|
||||
<span style={{ marginLeft: "10px" }}>{elementColor}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Design;
|
||||
@@ -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 = () => {
|
||||
</div>
|
||||
<div className="module">Visualization</div>
|
||||
</div>
|
||||
<div
|
||||
className={`module-list ${
|
||||
activeModule === "market" && "active"
|
||||
}`}
|
||||
onClick={() => setActiveModule("market")}
|
||||
>
|
||||
<div className="icon">
|
||||
<CartIcon isActive={activeModule === "market"} />
|
||||
</div>
|
||||
<div className="module">Market Place</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
187
app/src/components/ui/componets/AddButtons.tsx
Normal file
187
app/src/components/ui/componets/AddButtons.tsx
Normal file
@@ -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<ButtonsProps> = ({
|
||||
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 (
|
||||
<div>
|
||||
{(["top", "right", "bottom", "left"] as Side[]).map((side) => (
|
||||
<div key={side} className={`side-button-container ${side}`}>
|
||||
{/* "+" Button */}
|
||||
<button
|
||||
className={`side-button ${side}`}
|
||||
onClick={() => handlePlusButtonClick(side)}
|
||||
title={
|
||||
selectedZone.activeSides.includes(side)
|
||||
? `Remove all items and close ${side} panel`
|
||||
: `Activate ${side} panel`
|
||||
}
|
||||
>
|
||||
+
|
||||
</button>
|
||||
|
||||
{/* Extra Buttons */}
|
||||
<div
|
||||
className="extra-Bs"
|
||||
style={{
|
||||
display: selectedZone.activeSides.includes(side)
|
||||
? "flex"
|
||||
: "none",
|
||||
}}
|
||||
>
|
||||
{/* Hide Panel */}
|
||||
<div
|
||||
className="icon"
|
||||
title="Hide Panel"
|
||||
onClick={() => toggleVisibility(side)}
|
||||
>
|
||||
<EyeIcon />
|
||||
</div>
|
||||
|
||||
{/* Clean Panel */}
|
||||
<div
|
||||
className="icon"
|
||||
title="Clean Panel"
|
||||
onClick={() => cleanPanel(side)}
|
||||
>
|
||||
<CleanPannel />
|
||||
</div>
|
||||
|
||||
{/* Lock/Unlock Panel */}
|
||||
<div
|
||||
className={`icon ${
|
||||
selectedZone.lockedPanels.includes(side) ? "active" : ""
|
||||
}`}
|
||||
title={
|
||||
selectedZone.lockedPanels.includes(side)
|
||||
? "Unlock Panel"
|
||||
: "Lock Panel"
|
||||
}
|
||||
onClick={() => toggleLockPanel(side)}
|
||||
>
|
||||
<LockIcon />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddButtons;
|
||||
217
app/src/components/ui/componets/DraggableWidget.tsx
Normal file
217
app/src/components/ui/componets/DraggableWidget.tsx
Normal file
@@ -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 (
|
||||
<>
|
||||
<div
|
||||
key={widget.id}
|
||||
className={`chart-container ${selectedChartId?.id === widget.id && "activeChart"}`}
|
||||
style={cardStyle} // Apply dynamic card styles here
|
||||
onPointerDown={handlePointerDown}
|
||||
onDoubleClick={handleDoubleClick} // Add double-click event
|
||||
>
|
||||
{widget.type === "progress" ? (
|
||||
// <ProgressCard title={widget.title} data={widget.data} />
|
||||
<></>
|
||||
) : (
|
||||
<ChartComponent
|
||||
type={widget.type}
|
||||
title={widget.title}
|
||||
fontFamily={customizationOptions.font} // Pass font customization to ChartComponent
|
||||
fontSize={widget.fontSize}
|
||||
fontWeight={widget.fontWeight}
|
||||
data={widget.data}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Popup for Customizing Template Theme */}
|
||||
{isPopupOpen && (
|
||||
<div className="popup-overlay">
|
||||
<div className="popup-content">
|
||||
<h2>Customize Template Theme</h2>
|
||||
<div className="form-group">
|
||||
<label>Template Background</label>
|
||||
<input
|
||||
type="color"
|
||||
value={customizationOptions.templateBackground}
|
||||
onChange={(e) =>
|
||||
setCustomizationOptions((prev) => ({
|
||||
...prev,
|
||||
templateBackground: e.target.value,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>Card Background</label>
|
||||
<input
|
||||
type="color"
|
||||
value={customizationOptions.cardBackground}
|
||||
onChange={(e) =>
|
||||
setCustomizationOptions((prev) => ({
|
||||
...prev,
|
||||
cardBackground: e.target.value,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>Card Opacity</label>
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="1"
|
||||
step="0.1"
|
||||
value={customizationOptions.cardOpacity}
|
||||
onChange={(e) =>
|
||||
setCustomizationOptions((prev) => ({
|
||||
...prev,
|
||||
cardOpacity: parseFloat(e.target.value),
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>Card Blur</label>
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="10"
|
||||
value={customizationOptions.cardBlur}
|
||||
onChange={(e) =>
|
||||
setCustomizationOptions((prev) => ({
|
||||
...prev,
|
||||
cardBlur: parseInt(e.target.value),
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>Font</label>
|
||||
<select
|
||||
value={customizationOptions.font}
|
||||
onChange={(e) =>
|
||||
setCustomizationOptions((prev) => ({
|
||||
...prev,
|
||||
font: e.target.value,
|
||||
}))
|
||||
}
|
||||
>
|
||||
<option value="Arial">Arial</option>
|
||||
<option value="Times New Roman">Times New Roman</option>
|
||||
<option value="Courier New">Courier New</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>Margin</label>
|
||||
<input
|
||||
type="number"
|
||||
value={customizationOptions.margin}
|
||||
onChange={(e) =>
|
||||
setCustomizationOptions((prev) => ({
|
||||
...prev,
|
||||
margin: parseInt(e.target.value),
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>Radius</label>
|
||||
<input
|
||||
type="number"
|
||||
value={customizationOptions.radius}
|
||||
onChange={(e) =>
|
||||
setCustomizationOptions((prev) => ({
|
||||
...prev,
|
||||
radius: parseInt(e.target.value),
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>Shadow</label>
|
||||
<select
|
||||
value={customizationOptions.shadow}
|
||||
onChange={(e) =>
|
||||
setCustomizationOptions((prev) => ({
|
||||
...prev,
|
||||
shadow: e.target.value,
|
||||
}))
|
||||
}
|
||||
>
|
||||
<option value="Low">Low</option>
|
||||
<option value="Medium">Medium</option>
|
||||
<option value="High">High</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="popup-actions">
|
||||
<button onClick={handleClosePopup}>Cancel</button>
|
||||
<button onClick={handleSaveChanges}>Save Changes</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
197
app/src/components/ui/componets/Panel.tsx
Normal file
197
app/src/components/ui/componets/Panel.tsx
Normal file
@@ -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<PanelProps> = ({ 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) => (
|
||||
<div
|
||||
key={side}
|
||||
className={`panel ${side}-panel absolute ${isPlaying && ""}`}
|
||||
style={getPanelStyle(side)}
|
||||
onDrop={(e) => handleDrop(e, side)}
|
||||
onDragOver={(e) => e.preventDefault()}
|
||||
ref={(el) => {
|
||||
if (el) {
|
||||
panelRefs.current[side] = el;
|
||||
} else {
|
||||
delete panelRefs.current[side];
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="panel-content"
|
||||
style={{
|
||||
pointerEvents: selectedZone.lockedPanels.includes(side)
|
||||
? "none"
|
||||
: "auto",
|
||||
opacity: selectedZone.lockedPanels.includes(side) ? "0.8" : "1",
|
||||
}}
|
||||
>
|
||||
{selectedZone.widgets
|
||||
.filter((w) => w.panel === side)
|
||||
.map((widget) => (
|
||||
<DraggableWidget widget={widget} key={widget.id} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Panel;
|
||||
// only load selected template
|
||||
|
||||
207
app/src/components/ui/componets/RealTimeVisulization.tsx
Normal file
207
app/src/components/ui/componets/RealTimeVisulization.tsx
Normal file
@@ -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<HTMLDivElement>(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<string | null> => {
|
||||
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<HTMLImageElement>((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 (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="realTime-viz canvas"
|
||||
style={{
|
||||
height: isPlaying ? "100vh" : "600px",
|
||||
width: isPlaying ? "100%" : "55%",
|
||||
left: isPlaying ? "50%" : "48%",
|
||||
}}
|
||||
>
|
||||
<div className="realTimeViz-tools">
|
||||
<div
|
||||
className="icon save"
|
||||
title="Save Template"
|
||||
onClick={handleSaveTemplate}
|
||||
>
|
||||
<SaveTeemplateIcon />
|
||||
</div>
|
||||
<div className="icon comment" title="Comment">
|
||||
<CommentIcon />
|
||||
</div>
|
||||
<div
|
||||
className="play icon"
|
||||
title="Play"
|
||||
onClick={() => setIsPlaying(!isPlaying)}
|
||||
>
|
||||
<PlayIcon />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={`zoon-wrapper ${selectedZone.activeSides.includes("bottom") && "bottom"}`}>
|
||||
{Object.keys(zonesData).map((zoneName, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`zone ${selectedZone.zoneName === zoneName ? "active" : ""}`}
|
||||
onClick={() => {
|
||||
setSelectedZone({
|
||||
zoneName,
|
||||
...zonesData[zoneName],
|
||||
});
|
||||
}}
|
||||
>
|
||||
{zoneName}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{!isPlaying && (
|
||||
<AddButtons
|
||||
selectedZone={selectedZone}
|
||||
setSelectedZone={setSelectedZone}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Panel selectedZone={selectedZone} setSelectedZone={setSelectedZone} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default RealTimeVisulization;
|
||||
94
app/src/components/ui/inputs/MultiLevelDropDown.tsx
Normal file
94
app/src/components/ui/inputs/MultiLevelDropDown.tsx
Normal file
@@ -0,0 +1,94 @@
|
||||
import React, { useState } from "react";
|
||||
|
||||
|
||||
// Dropdown Item Component
|
||||
const DropdownItem = ({
|
||||
label,
|
||||
href,
|
||||
onClick,
|
||||
}: {
|
||||
label: string;
|
||||
href?: string;
|
||||
onClick?: () => void;
|
||||
}) => (
|
||||
<a
|
||||
href={href || "#"}
|
||||
className="dropdown-item"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
onClick?.();
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</a>
|
||||
);
|
||||
|
||||
// Nested Dropdown Component
|
||||
const NestedDropdown = ({
|
||||
label,
|
||||
children,
|
||||
}: {
|
||||
label: string;
|
||||
children: React.ReactNode;
|
||||
}) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="nested-dropdown">
|
||||
{/* Dropdown Trigger */}
|
||||
<div
|
||||
className="dropdown-trigger"
|
||||
onClick={() => setOpen(!open)} // Toggle submenu on click
|
||||
>
|
||||
{label} <span className="icon">{open ? "▼" : "▶"}</span>
|
||||
</div>
|
||||
|
||||
{/* Submenu */}
|
||||
{open && <div className="submenu">{children}</div>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Recursive Function to Render Nested Data
|
||||
const renderNestedData = (data: Record<string, any>) => {
|
||||
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 (
|
||||
<NestedDropdown key={key} label={key}>
|
||||
{renderNestedData(value)}
|
||||
</NestedDropdown>
|
||||
);
|
||||
} else if (Array.isArray(value)) {
|
||||
// If the value is an array, render each item as a dropdown item
|
||||
return value.map((item, index) => (
|
||||
<DropdownItem key={index} label={item} />
|
||||
));
|
||||
} else {
|
||||
// If the value is a simple string, render it as a dropdown item
|
||||
return <DropdownItem key={key} label={value} />;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Main Multi-Level Dropdown Component
|
||||
const MultiLevelDropdown = ({ data }: { data: Record<string, any> }) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="multi-level-dropdown">
|
||||
{/* Dropdown Trigger Button */}
|
||||
<button
|
||||
className="dropdown-button"
|
||||
onClick={() => setOpen(!open)} // Toggle main menu on click
|
||||
>
|
||||
Dropdown trigger <span className="icon">▼</span>
|
||||
</button>
|
||||
|
||||
{/* Dropdown Menu */}
|
||||
{open && <div className="dropdown-menu">{renderNestedData(data)}</div>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MultiLevelDropdown;
|
||||
82
app/src/components/ui/inputs/RegularDropDown.tsx
Normal file
82
app/src/components/ui/inputs/RegularDropDown.tsx
Normal file
@@ -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<DropdownProps> = ({
|
||||
header,
|
||||
options,
|
||||
onSelect,
|
||||
}) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [selectedOption, setSelectedOption] = useState<string | null>(null);
|
||||
const dropdownRef = useRef<HTMLDivElement>(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 (
|
||||
<div className="regularDropdown-container" ref={dropdownRef}>
|
||||
{/* Dropdown Header */}
|
||||
<div className="dropdown-header flex-sb" onClick={toggleDropdown}>
|
||||
<div className="key">{selectedOption || header}</div>
|
||||
<div className="icon">▾</div>
|
||||
</div>
|
||||
|
||||
{/* Dropdown Options */}
|
||||
{isOpen && (
|
||||
<div className="dropdown-options">
|
||||
{options.map((option, index) => (
|
||||
<div
|
||||
className="option"
|
||||
key={index}
|
||||
onClick={() => handleOptionClick(option)}
|
||||
>
|
||||
{option}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default RegularDropDown;
|
||||
@@ -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 (
|
||||
<div className="project-main">
|
||||
<ModuleToggle />
|
||||
<SideBarLeft />
|
||||
<SideBarRight />
|
||||
{activeModule === "visualization" && <RealTimeVisulization />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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<ModuleStore>((set) => ({
|
||||
activeModule: 'builder', // Initial state
|
||||
activeModule: "visualization", // Initial state
|
||||
setActiveModule: (module) => set({ activeModule: module }), // Update state
|
||||
}));
|
||||
|
||||
|
||||
11
app/src/store/usePlayButtonStore.ts
Normal file
11
app/src/store/usePlayButtonStore.ts
Normal file
@@ -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<PlayButtonStore>((set) => ({
|
||||
isPlaying: false, // Default state for play/pause
|
||||
setIsPlaying: (value) => set({ isPlaying: value }), // Update isPlaying state
|
||||
}));
|
||||
39
app/src/store/useTemplateStore.ts
Normal file
39
app/src/store/useTemplateStore.ts
Normal file
@@ -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<TemplateStore>((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;
|
||||
11
app/src/store/useThemeStore.ts
Normal file
11
app/src/store/useThemeStore.ts
Normal file
@@ -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<ThemeState>((set) => ({
|
||||
themeColor: ["#5c87df", "#EEEEFE", "#969BA7"],
|
||||
setThemeColor: (colors) => set({ themeColor: colors }),
|
||||
}));
|
||||
49
app/src/store/useWidgetStore.ts
Normal file
49
app/src/store/useWidgetStore.ts
Normal file
@@ -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<WidgetStore>((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 }),
|
||||
}));
|
||||
41
app/src/store/useZoneStore.ts
Normal file
41
app/src/store/useZoneStore.ts
Normal file
@@ -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<SelectedZoneState> | ((prev: SelectedZoneState) => SelectedZoneState)) => void;
|
||||
}
|
||||
|
||||
export const useSelectedZoneStore = create<SelectedZoneStore>((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
|
||||
})),
|
||||
}));
|
||||
52
app/src/styles/components/_regularDropDown.scss
Normal file
52
app/src/styles/components/_regularDropDown.scss
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
464
app/src/styles/pages/realTimeViz.scss
Normal file
464
app/src/styles/pages/realTimeViz.scss
Normal file
@@ -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%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user