diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 1fe56d2..fdb156e 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -13,8 +13,12 @@ "windows": [ { "title": "Central LRX", - "width": 800, - "height": 600 + "decorations": false, + "width": 1600, + "height": 900, + "minWidth": 1280, + "minHeight": 720, + "maximized": true } ], "security": { diff --git a/src/app/App.tsx b/src/app/App.tsx index 3d78e6d..93cd388 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -11,6 +11,7 @@ import { ChartWindowPage } from "../features/chartworkspace/pages/ChartWindowPag import { SettingsPage } from "../features/settings/pages/SettingsPage"; import SynopticPage from "../features/synoptic/pages/SynopticPage"; import MeteoChartsPage from "../features/meteo/pages/MeteoChartsPage"; +import { TitleBar } from "../components/window/TitleBar"; export type AppPage = | "dashboard" @@ -43,52 +44,56 @@ function App() { } return ( - - {({ theme }) => { - if (activePage === "meteo") { - return ( - setActivePage("meteoCharts")} - /> - ); - } +
+ - if (activePage === "meteoCharts") { - return ( - - ); - } +
+
+ + {({ theme }) => { + if (activePage === "meteo") { + return ( + setActivePage("meteoCharts")} + /> + ); + } - if (activePage === "climateCharts") { - return ; - } + if (activePage === "meteoCharts") { + return ; + } - if (activePage === "console") return ; + if (activePage === "climateCharts") { + return ; + } - if (activePage === "maincharts") { - return ; - } + if (activePage === "console") return ; - if (activePage === "settings") { - return ; - } + if (activePage === "maincharts") { + return ; + } - if (activePage === "synoptic") { - return ; - } + if (activePage === "settings") { + return ; + } - return ( - setActivePage("meteo")} - onNavigate={setActivePage} - /> - ); - }} - + if (activePage === "synoptic") { + return ; + } + + return ( + setActivePage("meteo")} + onNavigate={setActivePage} + /> + ); + }} + +
+
+
); } diff --git a/src/assets/icontestph.png b/src/assets/icontestph.png new file mode 100644 index 0000000..a6a65c4 Binary files /dev/null and b/src/assets/icontestph.png differ diff --git a/src/components/charts/WorkspaceChart.tsx b/src/components/charts/WorkspaceChart.tsx index bb972c0..3fbf711 100644 --- a/src/components/charts/WorkspaceChart.tsx +++ b/src/components/charts/WorkspaceChart.tsx @@ -64,6 +64,7 @@ type Props = { theme: "dark" | "light"; chart: WorkspaceChartConfig; detached?: boolean; + compact?: boolean; dragHandle?: React.ReactNode; onHeaderPointerDown?: (event: React.PointerEvent) => void; onModeChange: (mode: WorkspaceChartMode) => void; @@ -93,6 +94,8 @@ type YAxisConfig = { }; const RADIUS = "rounded-[8px]"; +const THIN_X_SCROLLBAR = "[scrollbar-width:thin] [scrollbar-color:#33445F_transparent] [&::-webkit-scrollbar]:h-1 [&::-webkit-scrollbar-track]:bg-transparent [&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-thumb]:bg-[#33445F]"; +const THIN_Y_SCROLLBAR = "[scrollbar-width:thin] [scrollbar-color:#33445F_transparent] [&::-webkit-scrollbar]:w-1 [&::-webkit-scrollbar-track]:bg-transparent [&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-thumb]:bg-[#33445F]"; const RANGE_OPTIONS: { value: WorkspaceChartTimeRange; label: string }[] = [ { value: "15m", label: "15M" }, @@ -115,6 +118,7 @@ export function WorkspaceChart({ chart, loading = false, detached = false, + compact = false, dragHandle, onHeaderPointerDown, onModeChange, @@ -267,9 +271,7 @@ export function WorkspaceChart({ }; }, [primaryVariable]); - const yDomain: [number | "auto", number | "auto"] = zeroBaseline - ? [0, "auto"] - : ["auto", "auto"]; + const yDomain = useMemo(() => buildPaddedYDomain(zeroBaseline), [zeroBaseline]); const allowedIntervalOptions = useMemo(() => { if (chart.mode === "bar") { @@ -286,16 +288,20 @@ export function WorkspaceChart({ className={ detached ? isDark - ? `${RADIUS} flex h-full min-h-0 flex-col overflow-hidden border-0 bg-[#0F1726] text-slate-100` - : `${RADIUS} flex h-full min-h-0 flex-col overflow-hidden border-0 bg-white text-slate-950` + ? `${RADIUS} flex h-full min-h-0 flex-col overflow-visible border-0 bg-[#0F1726] text-slate-100` + : `${RADIUS} flex h-full min-h-0 flex-col overflow-visible border-0 bg-white text-slate-950` : isDark - ? `${RADIUS} flex h-full min-h-0 flex-col overflow-hidden border border-[#223049] bg-[#0F1726] text-slate-100 shadow-[0_18px_50px_rgba(0,0,0,0.24)]` - : `${RADIUS} flex h-full min-h-0 flex-col overflow-hidden border border-slate-200 bg-white text-slate-950 shadow-[0_14px_34px_rgba(15,23,42,0.08)]` + ? `${RADIUS} flex h-full min-h-0 flex-col overflow-visible border border-[#223049] bg-[#0F1726] text-slate-100 shadow-[0_18px_50px_rgba(0,0,0,0.24)]` + : `${RADIUS} flex h-full min-h-0 flex-col overflow-visible border border-slate-200 bg-white text-slate-950 shadow-[0_14px_34px_rgba(15,23,42,0.08)]` } >
{dragHandle &&
{dragHandle}
} @@ -304,7 +310,11 @@ export function WorkspaceChart({
{chart.title ? (
-

+

{chart.title}

@@ -318,7 +328,7 @@ export function WorkspaceChart({
) : null} - {chart.subtitle ? ( + {chart.subtitle && !compact ? (

{chart.subtitle}

@@ -363,22 +373,34 @@ export function WorkspaceChart({
-
+
-
+
@@ -415,7 +437,7 @@ export function WorkspaceChart({ onClick={() => setIntervalMenuOpen((value) => !value)} className={buttonClass(isDark)} > - Intervalo: {formatIntervalLabel(chart.interval)} + {compact ? formatIntervalLabel(chart.interval) : `Intervalo: ${formatIntervalLabel(chart.interval)}`} @@ -451,7 +473,7 @@ export function WorkspaceChart({ onClick={() => setModeMenuOpen((value) => !value)} className={buttonClass(isDark)} > - Tipo: {formatModeLabel(chart.mode)} + {compact ? formatModeLabel(chart.mode) : `Tipo: ${formatModeLabel(chart.mode)}`} @@ -492,8 +514,8 @@ export function WorkspaceChart({ : buttonClass(isDark) } > - - Indicadores + + {!compact && "Indicadores"}
@@ -515,7 +537,7 @@ export function WorkspaceChart({ Variáveis registadas

-
+
{chart.variables.map((variable) => (
-
-
+
+
{visibleVariables.map((variable) => ( {formatValue(stats.average, displayUnit(primaryVariable.unit))} - - Máx:{" "} - {formatValue(stats.max, displayUnit(primaryVariable.unit))} - - - Mín:{" "} - {formatValue(stats.min, displayUnit(primaryVariable.unit))} - + {!compact && ( + <> + + Máx:{" "} + {formatValue(stats.max, displayUnit(primaryVariable.unit))} + + + Mín:{" "} + {formatValue(stats.min, displayUnit(primaryVariable.unit))} + + + )}
)}
-
+
{visibleVariables.length === 0 && !shouldShowLoading ? ( ) : data.length === 0 && !shouldShowLoading ? ( @@ -616,12 +646,15 @@ export function WorkspaceChart({ ) : data.length > 0 ? ( {chart.mode === "bar" ? ( - + {visibleVariables.map((variable) => ( ))} ) : chart.mode === "area" ? ( - + {renderReferenceLines( visibleVariables, showIndicators, stats.average, + yAxes, + primaryVariable, )} {visibleVariables.map((variable) => ( ))} ) : ( - + {renderReferenceLines( visibleVariables, showIndicators, stats.average, + yAxes, + primaryVariable, )} {visibleVariables.map((variable) => ( @@ -714,8 +757,8 @@ export function WorkspaceChart({ )}
- {showIndicators && primaryVariable && ( -
+ {showIndicators && primaryVariable && !compact && ( +
1 ? 8 : 4, bottom: 2, left: 0 } + : { top: 18, right: axisCount > 1 ? 24 : 16, bottom: 10, left: 10 }; +} + +function buildPaddedYDomain(zeroBaseline: boolean): [(dataMin: number) => number, (dataMax: number) => number] { + return [ + (dataMin: number) => { + if (!Number.isFinite(dataMin)) return zeroBaseline ? 0 : 0; + if (zeroBaseline) return 0; + + return padYDomainEdge(dataMin, "min"); + }, + (dataMax: number) => { + if (!Number.isFinite(dataMax)) return 1; + + return padYDomainEdge(dataMax, "max"); + }, + ]; +} + +function padYDomainEdge(value: number, edge: "min" | "max") { + const magnitude = Math.max(Math.abs(value), 1); + const padding = magnitude * 0.08; + + if (edge === "min") { + return value - padding; + } + + return value + padding; +} + +function getXAxisPaddingMs( + interval: WorkspaceChartInterval, + mode: WorkspaceChartMode, +) { + const intervalMs = intervalToMs(interval); + + if (!Number.isFinite(intervalMs) || intervalMs <= 0) { + return 0; + } + + return mode === "bar" ? intervalMs * 0.75 : intervalMs * 0.5; +} + function ChartScaffold({ isDark, yDomain, yAxes, - chartTimeRange + chartTimeRange, + chartInterval, + chartMode, + compact = false, }: { isDark: boolean; - yDomain: [number | "auto", number | "auto"]; + yDomain: [(dataMin: number) => number, (dataMax: number) => number]; yAxes: YAxisConfig[]; chartTimeRange: WorkspaceChartTimeRange; + chartInterval: WorkspaceChartInterval; + chartMode: WorkspaceChartMode; + compact?: boolean; }) { + const xPadMs = getXAxisPaddingMs(chartInterval, chartMode); + return ( <> dataMin - xPadMs, + (dataMax: number) => dataMax + xPadMs, + ]} + padding={{ left: 0, right: 0 }} + allowDataOverflow={false} + tick={{ fill: "#64748b", fontSize: compact ? 9 : 10 }} tickLine={false} axisLine={{ stroke: isDark ? "rgba(148,163,184,0.12)" : "#cbd5e1", }} - minTickGap={34} + minTickGap={compact ? 20 : 34} tickFormatter={(value) => formatAxisTime(new Date(Number(value)).toISOString(), chartTimeRange) } @@ -798,12 +899,15 @@ function ChartScaffold({ yAxisId={axis.id} orientation={axis.orientation} domain={yDomain} - tick={{ fill: axis.color, fontSize: 10 }} + allowDataOverflow={false} + tick={{ fill: axis.color, fontSize: compact ? 9 : 10 }} tickLine={false} + tickMargin={compact ? 4 : 6} + tickFormatter={(value) => formatDecimal(Number(value), 1)} axisLine={{ stroke: isDark ? "rgba(148,163,184,0.18)" : "#cbd5e1", }} - width={52} + width={compact ? 44 : 52} label={ axis.unit ? { @@ -814,7 +918,7 @@ function ChartScaffold({ ? "insideLeft" : "insideRight", fill: axis.color, - fontSize: 10, + fontSize: compact ? 9 : 10, } : undefined } @@ -822,6 +926,7 @@ function ChartScaffold({ ))} { + const numericValue = typeof value === "number" ? value : Number(value); + + return [ + formatDecimal(numericValue, 2), + String(name ?? ""), + ]; + }} contentStyle={{ background: isDark ? "#111827" : "#ffffff", border: isDark @@ -858,8 +971,8 @@ function DropdownPanel({
{children} @@ -879,15 +992,19 @@ function renderReferenceLines( visibleVariables: WorkspaceChartVariable[], showIndicators: boolean, average: number | null, + yAxes: YAxisConfig[], + primaryVariable: WorkspaceChartVariable | null, ) { return ( <> - {showIndicators && average !== null && ( + {showIndicators && average !== null && primaryVariable && ( )} @@ -895,10 +1012,12 @@ function renderReferenceLines( variable.limit !== undefined ? ( ) : null, )} @@ -955,21 +1074,26 @@ function windowButtonClass(isDark: boolean) { : `${RADIUS} p-2 text-slate-400 transition hover:bg-slate-100 hover:text-slate-900`; } +function formatDecimal(value: number | null | undefined, decimals = 2) { + if (value === null || value === undefined || !Number.isFinite(value)) return "--"; + + return value.toFixed(decimals); +} + function formatValue(value: number | null, unit?: string) { - if (value === null || Number.isNaN(value)) return "--"; - const normalizedUnit = displayUnit(unit); + const formattedValue = formatDecimal(value, 2); - return `${value.toFixed(1)}${normalizedUnit ? ` ${normalizedUnit}` : ""}`; + return `${formattedValue}${formattedValue !== "--" && normalizedUnit ? ` ${normalizedUnit}` : ""}`; } function formatSignedValue(value: number | null, unit?: string) { - if (value === null || Number.isNaN(value)) return "--"; + if (value === null || !Number.isFinite(value)) return "--"; const prefix = value >= 0 ? "+" : ""; const normalizedUnit = displayUnit(unit); - return `${prefix}${value.toFixed(1)}${normalizedUnit ? ` ${normalizedUnit}` : ""}`; + return `${prefix}${formatDecimal(value, 2)}${normalizedUnit ? ` ${normalizedUnit}` : ""}`; } function formatRangeLabel(range: WorkspaceChartTimeRange) { return range.toUpperCase(); diff --git a/src/components/layout/AppShell.tsx b/src/components/layout/AppShell.tsx index 3c87a1b..2f5c0e3 100644 --- a/src/components/layout/AppShell.tsx +++ b/src/components/layout/AppShell.tsx @@ -1,8 +1,6 @@ import { type ReactNode, useEffect, useState } from "react"; import { Sidebar } from "../navigation/Sidebar"; -import { TopBar } from "../layout/TopBar"; import { useTelemetryStream } from "../../features/telemetry/hooks/useTelemetryStream"; -import { useNotifications } from "../../features/notifications/hooks/useNotifications"; import { useCurrentUser } from "../../features/auth/hooks/useCurrentUser"; import type { TelemetrySnapshot } from "../../types/telemetry"; import type { AppPage } from "../../app/App"; @@ -24,10 +22,11 @@ const THEME_STORAGE_KEY = "app-theme"; export function AppShell({ activePage, onNavigate, children }: AppShellProps) { const telemetry = useTelemetryStream(); - const notifications = useNotifications(); const currentUser = useCurrentUser(); const [sidebarCollapsed, setSidebarCollapsed] = useState(false); + const [autoCompact, setAutoCompact] = useState(false); + const [theme, setTheme] = useState(() => { const stored = localStorage.getItem(THEME_STORAGE_KEY); return stored === "light" || stored === "dark" ? stored : "dark"; @@ -44,12 +43,28 @@ export function AppShell({ activePage, onNavigate, children }: AppShellProps) { setTheme((current) => (current === "dark" ? "light" : "dark")); }; + useEffect(() => { + const update = () => { + const compact = window.innerWidth <= 1366 || window.innerHeight <= 760; + setAutoCompact(compact); + + if (compact) { + setSidebarCollapsed(true); + } + }; + + update(); + window.addEventListener("resize", update); + + return () => window.removeEventListener("resize", update); + }, []); + return (