feat(charts): add persistent workspace chart management

This commit is contained in:
litoral05
2026-05-27 14:38:25 +01:00
parent d7ef36fc53
commit ffe3c64cfa
23 changed files with 4407 additions and 202 deletions
+56 -48
View File
@@ -28,7 +28,7 @@ type SidebarProps = {
onToggleCollapsed: () => void;
};
const RADIUS = "rounded-[10px]";
const RADIUS = "rounded-[6px]";
const navigationItems: {
label: string;
@@ -37,6 +37,7 @@ const navigationItems: {
}[] = [
{ label: "Painel Principal", page: "dashboard", icon: Home },
{ label: "Meteorologia", page: "meteo", icon: CloudSun },
{ label: "Gráficos Gerais", page: "maincharts", icon: BarChart3 }
];
const climateItems = [
@@ -55,10 +56,14 @@ const irrigationItems = [
{ label: "Gráficos", icon: BarChart3 },
];
const utilityItems = [
{ label: "Consola (VNC)", icon: TabletSmartphone },
{ label: "Configurações", icon: Settings },
];
const utilityItems: {
label: string;
page: AppPage;
icon: React.ElementType;
}[] = [
{ label: "Consola (VNC)", page: "console", icon: TabletSmartphone },
{ label: "Configurações", page: "settings", icon: Settings },
];
export function Sidebar({
theme,
@@ -75,6 +80,10 @@ export function Sidebar({
const handleTreeClick = (key: string) => {
setActiveTreeItem(key);
if (key === "climate:Gráficos") {
onNavigate("climateCharts");
}
};
const handleTreeToggle = (section: "climate" | "irrigation") => {
@@ -96,28 +105,30 @@ export function Sidebar({
<aside
className={
isDark
? `${collapsed ? "w-20" : "w-[290px]"} flex h-full flex-col border-r border-white/10 bg-[#0F172A] px-4 py-5 text-slate-100 shadow-[8px_0_30px_rgba(0,0,0,0.18)] transition-all duration-200`
: `${collapsed ? "w-20" : "w-[290px]"} flex h-full flex-col border-r border-[#D8DEE7] bg-[#F8FAFC] px-4 py-5 text-[#0F172A] shadow-[8px_0_28px_rgba(15,23,42,0.04)] transition-all duration-200`
? `${collapsed ? "w-20" : "w-[290px]"
} flex h-full flex-col border-r border-[#263247] bg-[#0B1220] px-4 py-5 text-slate-100 transition-all duration-200`
: `${collapsed ? "w-20" : "w-[290px]"
} flex h-full flex-col border-r border-[#D7DEE8] bg-[#F3F6FA] px-4 py-5 text-[#0F172A] transition-all duration-200`
}
>
<div
className={
collapsed
? "mb-12 flex items-center justify-center"
: "mb-12 flex items-center gap-4 px-1"
? "mb-10 flex items-center justify-center"
: "mb-10 flex items-center gap-3 px-1"
}
>
<div
className={
isDark
? `${RADIUS} flex h-12 w-12 shrink-0 items-center justify-center overflow-hidden border border-white/10 bg-white/[0.04] shadow-[0_10px_24px_rgba(0,0,0,0.18)]`
: `${RADIUS} flex h-12 w-12 shrink-0 items-center justify-center overflow-hidden border border-[#D8DEE7] bg-white shadow-sm`
? `${RADIUS} flex h-12 w-12 shrink-0 items-center justify-center border border-[#2A3950] bg-[#111A2B] p-1.5`
: `${RADIUS} flex h-12 w-12 shrink-0 items-center justify-center border border-[#CBD5E1] bg-white p-1.5`
}
>
<img
src={logo}
alt="Litoral Central"
className="h-12 w-12 scale-[1.35] object-cover"
className="h-full w-full object-contain"
/>
</div>
@@ -126,21 +137,21 @@ export function Sidebar({
<div
className={
isDark
? "truncate text-[16px] font-black tracking-[-0.02em] text-white"
: "truncate text-[16px] font-black tracking-[-0.02em] text-[#0F172A]"
? "truncate text-[15px] font-extrabold tracking-[-0.02em] text-white"
: "truncate text-[15px] font-extrabold tracking-[-0.02em] text-[#0F172A]"
}
>
Litoral Central
</div>
<div className="mt-1 whitespace-nowrap text-[10px] font-bold uppercase tracking-[0.1em] text-slate-500">
<div className="mt-1 whitespace-nowrap text-[10px] font-bold uppercase tracking-[0.18em] text-[#7D8EA8]">
Operações agrícolas
</div>
</div>
)}
</div>
<nav className="space-y-2">
<nav className="space-y-1.5">
{navigationItems.map((item) => {
const Icon = item.icon;
const active = activePage === item.page && activeTreeItem === null;
@@ -157,9 +168,7 @@ export function Sidebar({
className={navButtonClass(isDark, active, collapsed)}
>
{active && <ActiveIndicator isDark={isDark} />}
<Icon className={navIconClass(isDark, active)} />
{!collapsed && <span className="truncate">{item.label}</span>}
</button>
);
@@ -197,21 +206,21 @@ export function Sidebar({
{utilityItems.map((item) => {
const Icon = item.icon;
const key = `utility:${item.label}`;
const active = activeTreeItem === key;
const active = activePage === item.page && activeTreeItem === null;
return (
<button
key={item.label}
type="button"
onClick={() => handleTreeClick(key)}
onClick={() => {
setActiveTreeItem(null);
onNavigate(item.page);
}}
title={collapsed ? item.label : undefined}
className={navButtonClass(isDark, active, collapsed)}
>
{active && <ActiveIndicator isDark={isDark} />}
<Icon className={navIconClass(isDark, active)} />
{!collapsed && <span className="truncate">{item.label}</span>}
</button>
);
@@ -223,15 +232,15 @@ export function Sidebar({
onClick={onToggleCollapsed}
className={
isDark
? `mt-auto flex items-center justify-center gap-2 ${RADIUS} border border-white/10 bg-white/[0.04] px-4 py-3.5 text-[15px] font-semibold text-slate-400 shadow-[0_10px_24px_rgba(0,0,0,0.12)] transition hover:bg-white/[0.06] hover:text-white`
: `mt-auto flex items-center justify-center gap-2 ${RADIUS} border border-[#D8DEE7] bg-white px-4 py-3.5 text-[15px] font-semibold text-slate-500 shadow-sm transition hover:bg-[#F1F5F9] hover:text-[#0F172A]`
? `mt-auto flex items-center justify-center gap-2 ${RADIUS} border border-[#263247] bg-[#111A2B] px-4 py-3 text-[14px] font-semibold text-[#9BAAC1] transition hover:border-[#33445F] hover:bg-[#162033] hover:text-white`
: `mt-auto flex items-center justify-center gap-2 ${RADIUS} border border-[#CBD5E1] bg-white px-4 py-3 text-[14px] font-semibold text-slate-600 transition hover:bg-[#EAF0F7] hover:text-[#0F172A]`
}
>
{collapsed ? (
<ChevronRight className="h-[22px] w-[22px]" />
<ChevronRight className="h-5 w-5" />
) : (
<>
<ChevronLeft className="h-[22px] w-[22px]" />
<ChevronLeft className="h-5 w-5" />
<span>Recolher menu</span>
</>
)}
@@ -277,7 +286,6 @@ function TreeSection({
className={navButtonClass(isDark, hasActiveChild, collapsed)}
>
{hasActiveChild && <ActiveIndicator isDark={isDark} />}
<Icon className={navIconClass(isDark, hasActiveChild)} />
{!collapsed && (
@@ -285,7 +293,7 @@ function TreeSection({
<span className="min-w-0 flex-1 truncate">{label}</span>
<ChevronDown
className={`h-4 w-4 shrink-0 text-slate-500 transition-transform duration-200 ${open ? "rotate-180" : ""
className={`h-4 w-4 shrink-0 text-[#6F819B] transition-transform duration-200 ${open ? "rotate-180" : ""
}`}
/>
</>
@@ -296,8 +304,8 @@ function TreeSection({
<div
className={
isDark
? "ml-5 mt-2 space-y-1.5 border-l border-white/10 pl-3"
: "ml-5 mt-2 space-y-1.5 border-l border-[#D8DEE7] pl-3"
? "ml-[18px] mt-2 space-y-1 border-l border-[#263247] pl-3"
: "ml-[18px] mt-2 space-y-1 border-l border-[#CBD5E1] pl-3"
}
>
{items.map((item) => {
@@ -313,18 +321,18 @@ function TreeSection({
className={
active
? isDark
? "flex w-full items-center gap-2 rounded-[8px] bg-white/[0.06] px-3.5 py-2.5 text-left text-[13px] font-bold text-white"
: "flex w-full items-center gap-2 rounded-[8px] bg-white px-3.5 py-2.5 text-left text-[13px] font-bold text-[#0F172A] shadow-sm"
? "flex w-full items-center gap-2 rounded-[5px] border border-[#2C3D56] bg-[#131D2F] px-3 py-2.5 text-left text-[13px] font-bold text-white"
: "flex w-full items-center gap-2 rounded-[5px] border border-[#CBD5E1] bg-white px-3 py-2.5 text-left text-[13px] font-bold text-[#0F172A]"
: isDark
? "flex w-full items-center gap-2 rounded-[8px] px-3.5 py-2.5 text-left text-[13px] font-semibold text-slate-500 transition hover:bg-white/[0.04] hover:text-slate-200"
: "flex w-full items-center gap-2 rounded-[8px] px-3.5 py-2.5 text-left text-[13px] font-semibold text-slate-500 transition hover:bg-white hover:text-[#0F172A]"
? "flex w-full items-center gap-2 rounded-[5px] px-3 py-2.5 text-left text-[13px] font-semibold text-[#7D8EA8] transition hover:bg-[#111A2B] hover:text-slate-200"
: "flex w-full items-center gap-2 rounded-[5px] px-3 py-2.5 text-left text-[13px] font-semibold text-slate-500 transition hover:bg-white hover:text-[#0F172A]"
}
>
<SubIcon
className={
active
? isDark
? "h-4 w-4 shrink-0 text-emerald-300"
? "h-4 w-4 shrink-0 text-[#4FD1C5]"
: "h-4 w-4 shrink-0 text-[#0F766E]"
: "h-4 w-4 shrink-0"
}
@@ -352,8 +360,8 @@ function SectionLabel({
}
return (
<div className="py-3">
<p className="px-4 text-[11px] font-bold uppercase tracking-[0.18em] text-slate-500">
<div className="pb-1 pt-5">
<p className="px-4 text-[10px] font-black uppercase tracking-[0.2em] text-[#61738C]">
{label}
</p>
</div>
@@ -365,8 +373,8 @@ function ActiveIndicator({ isDark }: { isDark: boolean }) {
<span
className={
isDark
? "absolute left-0 top-1/2 h-7 w-1 -translate-y-1/2 rounded-r-full bg-emerald-400"
: "absolute left-0 top-1/2 h-7 w-1 -translate-y-1/2 rounded-r-full bg-[#0F766E]"
? "absolute left-0 top-1/2 h-6 w-[3px] -translate-y-1/2 rounded-r-sm bg-[#4FD1C5]"
: "absolute left-0 top-1/2 h-6 w-[3px] -translate-y-1/2 rounded-r-sm bg-[#0F766E]"
}
/>
);
@@ -377,23 +385,23 @@ function navButtonClass(isDark: boolean, active: boolean, collapsed: boolean) {
if (active) {
return isDark
? `relative flex w-full items-center gap-3 ${RADIUS} border border-emerald-400/20 bg-[#162033] ${alignment} py-[14px] text-left text-[15px] font-black text-white shadow-[0_12px_28px_rgba(0,0,0,0.22)]`
: `relative flex w-full items-center gap-3 ${RADIUS} border border-[#D8DEE7] bg-white ${alignment} py-[14px] text-left text-[15px] font-black text-[#0F172A] shadow-[0_8px_20px_rgba(15,23,42,0.06)]`;
? `relative flex w-full items-center gap-3 ${RADIUS} border border-[#2C3D56] bg-[#131D2F] ${alignment} py-3.5 text-left text-[14px] font-extrabold text-white`
: `relative flex w-full items-center gap-3 ${RADIUS} border border-[#CBD5E1] bg-white ${alignment} py-3.5 text-left text-[14px] font-extrabold text-[#0F172A]`;
}
return isDark
? `flex w-full items-center gap-3 ${RADIUS} ${alignment} py-[14px] text-left text-[15px] font-semibold text-slate-400 transition hover:bg-white/[0.04] hover:text-white`
: `flex w-full items-center gap-3 ${RADIUS} ${alignment} py-[14px] text-left text-[15px] font-semibold text-slate-600 transition hover:bg-white hover:text-[#0F172A] hover:shadow-sm`;
? `relative flex w-full items-center gap-3 ${RADIUS} ${alignment} py-3.5 text-left text-[14px] font-semibold text-[#8EA0BA] transition hover:bg-[#111A2B] hover:text-white`
: `relative flex w-full items-center gap-3 ${RADIUS} ${alignment} py-3.5 text-left text-[14px] font-semibold text-slate-600 transition hover:bg-white hover:text-[#0F172A]`;
}
function navIconClass(isDark: boolean, active: boolean) {
if (active) {
return isDark
? "h-[22px] w-[22px] shrink-0 text-emerald-300"
: "h-[22px] w-[22px] shrink-0 text-[#0F766E]";
? "h-5 w-5 shrink-0 text-[#4FD1C5]"
: "h-5 w-5 shrink-0 text-[#0F766E]";
}
return isDark
? "h-[22px] w-[22px] shrink-0 text-slate-500"
: "h-[22px] w-[22px] shrink-0 text-slate-500";
? "h-5 w-5 shrink-0 text-[#6F819B]"
: "h-5 w-5 shrink-0 text-slate-500";
}