From 0f87a0b840f8e042537713b6747372454b2f6351 Mon Sep 17 00:00:00 2001 From: Carter Date: Mon, 16 Mar 2026 14:10:47 -0600 Subject: [PATCH 1/5] adjusting column sizes will now make the table bigger or smaller if it has to --- src/common/ResponsiveTable.tsx | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/src/common/ResponsiveTable.tsx b/src/common/ResponsiveTable.tsx index b69589e..4c21901 100644 --- a/src/common/ResponsiveTable.tsx +++ b/src/common/ResponsiveTable.tsx @@ -16,8 +16,6 @@ const useStyles = makeStyles((theme: Theme) => { }, tableContainer: { width: "100%", - maxWidth: "100%", - overflow: "hidden", minWidth: 0, }, title: { @@ -550,14 +548,8 @@ export default function ResponsiveTable(props: Props) { if (actualDelta > 0) { // Shrinking current column – give space to next column newWidths[index + 1] = (newWidths[index + 1] ?? 0) + actualDelta - } else if (actualDelta < 0) { - // Growing current column – take from next column only what it can give - const nextWidth = newWidths[index + 1] ?? 0 - const availableFromNext = Math.max(0, nextWidth - MIN_COLUMN_WIDTH) - const takeFromNext = Math.min(-actualDelta, availableFromNext) - newWidths[index + 1] = nextWidth - takeFromNext - // If we couldn't take enough, the table grows (clampedWidth already applied) } + // When growing (actualDelta < 0): never shrink adjacent columns – table grows to accommodate setRowWidths(newWidths) } @@ -605,7 +597,20 @@ export default function ResponsiveTable(props: Props) { )} - +
0 + ? (() => { + const colsWidth = columns.reduce( + (sum, c, i) => sum + (filterList.includes(c.title) || c.hidden ? 0 : (rowWidths[i] ?? 0)), + 0 + ) + (rowSelect ? 48 : 0) + (renderGutter ? 48 : 0) + return colsWidth > 0 ? `max(${colsWidth}px, 100%)` : "100%" + })() + : "100%", + }} + > {customElement && From 141ece8a3efff31434058e84d60968c1566a2891 Mon Sep 17 00:00:00 2001 From: Carter Date: Mon, 16 Mar 2026 14:15:17 -0600 Subject: [PATCH 2/5] overlay overflow hidden so it stops affecting the scroll bar --- src/common/PulseBox.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/common/PulseBox.tsx b/src/common/PulseBox.tsx index 2cdc5ce..90c611c 100644 --- a/src/common/PulseBox.tsx +++ b/src/common/PulseBox.tsx @@ -8,6 +8,7 @@ const useStyles = makeStyles((_theme: Theme) => ({ position: 'relative', display: 'block', // Changed to block to respect Box margins width: 'fit-content', // Shrinks to child width + overflow: 'hidden', // Clip pulse animation so it doesn't affect parent layout '&::after': { content: '""', position: 'absolute', From 62763dd2914c05578baf9ca215ba349e4f4f71fb Mon Sep 17 00:00:00 2001 From: Carter Date: Mon, 16 Mar 2026 14:24:21 -0600 Subject: [PATCH 3/5] overlay overflow hidden so it stops affecting the scroll bar --- src/common/PulseBox.tsx | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/common/PulseBox.tsx b/src/common/PulseBox.tsx index 90c611c..f4ae696 100644 --- a/src/common/PulseBox.tsx +++ b/src/common/PulseBox.tsx @@ -8,7 +8,6 @@ const useStyles = makeStyles((_theme: Theme) => ({ position: 'relative', display: 'block', // Changed to block to respect Box margins width: 'fit-content', // Shrinks to child width - overflow: 'hidden', // Clip pulse animation so it doesn't affect parent layout '&::after': { content: '""', position: 'absolute', @@ -16,8 +15,7 @@ const useStyles = makeStyles((_theme: Theme) => ({ left: 0, width: '100%', height: '100%', - border: '2px solid var(--pulse-color)', - borderRadius: 'inherit', // Should inherit from Box + borderRadius: 'inherit', animation: '$pulse 1.75s infinite ease-in-out', pointerEvents: 'none', boxSizing: 'border-box', @@ -26,13 +24,13 @@ const useStyles = makeStyles((_theme: Theme) => ({ }, '@keyframes pulse': { from: { - transform: "scale(1)", - opacity: 1 + boxShadow: '0 0 0 2px var(--pulse-color)', + opacity: 1, }, to: { - transform: "scale(1.25)", - opacity: 0 - } + boxShadow: '0 0 0 14px var(--pulse-color)', + opacity: 0, + }, }, })); From 575c84246cb127da1bf06a1e78a8e88f11aaac8a Mon Sep 17 00:00:00 2001 From: Carter Date: Mon, 16 Mar 2026 14:41:18 -0600 Subject: [PATCH 4/5] gave the pulse box better animation pacing so the colors don't change in the middle of a pulse --- src/common/PulseBox.tsx | 72 +++++++++++++++++++++++-------------- src/common/StatusDust.tsx | 17 +-------- src/common/StatusGas.tsx | 17 +-------- src/common/StatusPlenum.tsx | 18 +--------- src/common/StatusSen5x.tsx | 17 +-------- 5 files changed, 50 insertions(+), 91 deletions(-) diff --git a/src/common/PulseBox.tsx b/src/common/PulseBox.tsx index f4ae696..eb5da3d 100644 --- a/src/common/PulseBox.tsx +++ b/src/common/PulseBox.tsx @@ -1,56 +1,76 @@ import { Box, BoxProps, Theme } from "@mui/material"; import { makeStyles } from "@mui/styles"; import classNames from "classnames"; -import { forwardRef, ReactNode } from "react"; +import { forwardRef, ReactNode, useCallback, useState } from "react"; const useStyles = makeStyles((_theme: Theme) => ({ pulseWrapper: { position: 'relative', - display: 'block', // Changed to block to respect Box margins - width: 'fit-content', // Shrinks to child width - '&::after': { - content: '""', - position: 'absolute', - top: 0, - left: 0, - width: '100%', - height: '100%', - borderRadius: 'inherit', - animation: '$pulse 1.75s infinite ease-in-out', - pointerEvents: 'none', - boxSizing: 'border-box', - transition: "border-color 0.35s ease-in-out", - }, + display: 'block', + width: 'fit-content', + }, + pulseRing: { + position: 'absolute', + top: 0, + left: 0, + width: '100%', + height: '100%', + borderRadius: 'inherit', + pointerEvents: 'none', + boxSizing: 'border-box', + animation: '$pulse 4.4s infinite ease-in-out', }, '@keyframes pulse': { - from: { - boxShadow: '0 0 0 2px var(--pulse-color)', - opacity: 1, - }, - to: { - boxShadow: '0 0 0 14px var(--pulse-color)', - opacity: 0, - }, + /* pulse, pause, pulse, pause - CONSISTENT: both pauses equal, both pulses equal */ + /* Pulse 1 (0-42%) */ + '0%': { boxShadow: '0 0 0 2px var(--pulse-color)', opacity: 0 }, + '2%': { boxShadow: '0 0 0 2px var(--pulse-color)', opacity: 1 }, + '40%': { boxShadow: '0 0 0 14px var(--pulse-color)', opacity: 0 }, + '42%': { boxShadow: '0 0 0 2px var(--pulse-color)', opacity: 0 }, + /* Pause 1 (42-50%) */ + '50%': { boxShadow: '0 0 0 2px var(--pulse-color)', opacity: 0 }, + /* Pulse 2 (50-92%) */ + '52%': { boxShadow: '0 0 0 2px var(--pulse-color)', opacity: 1 }, + '90%': { boxShadow: '0 0 0 14px var(--pulse-color)', opacity: 0 }, + '92%': { boxShadow: '0 0 0 2px var(--pulse-color)', opacity: 0 }, + /* Pause 2 (92-100%) - same length as Pause 1, color change at iteration */ + '100%': { boxShadow: '0 0 0 2px var(--pulse-color)', opacity: 0 }, }, })); interface Props extends BoxProps { color: string; + colors?: string[]; children: ReactNode; className?: string; } const PulseBox = forwardRef((props, ref) => { - const { color, children, className, ...boxProps } = props; + const { color, colors: colorsProp, children, className, ...boxProps } = props; const classes = useStyles() + const colors = colorsProp && colorsProp.length > 0 ? colorsProp : [color]; + const [colorIndex, setColorIndex] = useState(0); + const displayColor = colors[colorIndex % colors.length] ?? color; + + const onAnimationIteration = useCallback(() => { + if (colors.length > 1) { + setColorIndex((i) => (i + 1) % colors.length); + } + }, [colors.length]); + return ( + {children} ); diff --git a/src/common/StatusDust.tsx b/src/common/StatusDust.tsx index 4963269..c157501 100644 --- a/src/common/StatusDust.tsx +++ b/src/common/StatusDust.tsx @@ -57,21 +57,6 @@ export default function StatusDust(props: Props) { const colors = or(device.status.sen5x?.overlays.map(overlay => overlay.color), []); const messages = or(device.status.sen5x?.overlays.map(overlay => overlay.message), []); - const [color, setColor] = useState(colors.length > 0 ? colors[0] : ""); - const [, setColorIndex] = useState(0) - - useEffect(() => { - const interval = setInterval(() => { - setColorIndex(prevIndex => { - const newIndex = prevIndex >= colors.length - 1 ? 0 : prevIndex + 1; - setColor(colors[newIndex]); // Use the new index here - return newIndex; - }); - }, 7500); - - return () => clearInterval(interval); - }, []); - // const [currentIndex, setCurrentIndex] = useState(0); // Auto-cycle through measurements every 3 seconds @@ -142,7 +127,7 @@ export default function StatusDust(props: Props) { return ( - + overlay.message), []); } - const [color, setColor] = useState(colors.length > 0 ? colors[0] : ""); - const [, setColorIndex] = useState(0) - const gasTypes: ("o2" | "no2" | "co2" | "co" | "lel" | "h2s")[] = ["o2", "no2", "co2", "co", "lel", "h2s"] - useEffect(() => { - const interval = setInterval(() => { - setColorIndex(prevIndex => { - const newIndex = prevIndex >= colors.length - 1 ? 0 : prevIndex + 1; - setColor(colors[newIndex]); // Use the new index here - return newIndex; - }); - }, 7500); - - return () => clearInterval(interval); - }, []); - const [currentIndex, setCurrentIndex] = useState(0); // Auto-cycle through measurements every 5 seconds @@ -183,7 +168,7 @@ export default function StatusGas(props: Props) { } return ( - + { gasType === "all" && diff --git a/src/common/StatusPlenum.tsx b/src/common/StatusPlenum.tsx index 250660a..c7efef0 100644 --- a/src/common/StatusPlenum.tsx +++ b/src/common/StatusPlenum.tsx @@ -5,7 +5,6 @@ import { Device } from "models"; import PulseBox from "./PulseBox"; import RelativeTimestamp from "./RelativeTimestamp"; import { or } from "utils"; -import { useEffect, useState } from "react"; interface Props { device: Device; @@ -33,21 +32,6 @@ export default function StatusPlenum(props: Props) { const colors = or(device.status.plenum?.overlays.map(overlay => overlay.color), []); const messages = or(device.status.plenum?.overlays.map(overlay => overlay.message), []); - const [color, setColor] = useState(colors.length > 0 ? colors[0] : ""); - const [, setColorIndex] = useState(0) - - useEffect(() => { - const interval = setInterval(() => { - setColorIndex(prevIndex => { - const newIndex = prevIndex >= colors.length - 1 ? 0 : prevIndex + 1; - setColor(colors[newIndex]); // Use the new index here - return newIndex; - }); - }, 5000); - - return () => clearInterval(interval); - }, []); - const tooltip = () => { if (messages.length === 0) return null if (messages.length < 2) return ( @@ -107,7 +91,7 @@ export default function StatusPlenum(props: Props) { return ( - + diff --git a/src/common/StatusSen5x.tsx b/src/common/StatusSen5x.tsx index 533fde2..4e3d316 100644 --- a/src/common/StatusSen5x.tsx +++ b/src/common/StatusSen5x.tsx @@ -58,21 +58,6 @@ export default function StatusSen5x(props: Props) { const colors = or(device.status.sen5x?.overlays.map(overlay => overlay.color), []); const messages = or(device.status.sen5x?.overlays.map(overlay => overlay.message), []); - const [color, setColor] = useState(colors.length > 0 ? colors[0] : ""); - const [, setColorIndex] = useState(0) - - useEffect(() => { - const interval = setInterval(() => { - setColorIndex(prevIndex => { - const newIndex = prevIndex >= colors.length - 1 ? 0 : prevIndex + 1; - setColor(colors[newIndex]); // Use the new index here - return newIndex; - }); - }, 7500); - - return () => clearInterval(interval); - }, []); - const [currentIndex, setCurrentIndex] = useState(0); // Auto-cycle through measurements every 5 seconds @@ -147,7 +132,7 @@ export default function StatusSen5x(props: Props) { return ( - + Date: Mon, 16 Mar 2026 14:49:28 -0600 Subject: [PATCH 5/5] made the pulse box animation look nice by starting the pulse from behind the box that it's pulsing --- src/common/PulseBox.tsx | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/common/PulseBox.tsx b/src/common/PulseBox.tsx index eb5da3d..37ae123 100644 --- a/src/common/PulseBox.tsx +++ b/src/common/PulseBox.tsx @@ -8,6 +8,8 @@ const useStyles = makeStyles((_theme: Theme) => ({ position: 'relative', display: 'block', width: 'fit-content', + isolation: 'isolate', + overflow: 'visible', }, pulseRing: { position: 'absolute', @@ -18,23 +20,24 @@ const useStyles = makeStyles((_theme: Theme) => ({ borderRadius: 'inherit', pointerEvents: 'none', boxSizing: 'border-box', + zIndex: -1, animation: '$pulse 4.4s infinite ease-in-out', }, '@keyframes pulse': { + /* Start behind box, then expand out and fade */ /* pulse, pause, pulse, pause - CONSISTENT: both pauses equal, both pulses equal */ - /* Pulse 1 (0-42%) */ - '0%': { boxShadow: '0 0 0 2px var(--pulse-color)', opacity: 0 }, - '2%': { boxShadow: '0 0 0 2px var(--pulse-color)', opacity: 1 }, + /* Pulse 1 (0-42%) - gradual fade in to soften the start */ + '0%': { boxShadow: '0 0 0 -2px var(--pulse-color)', opacity: 1 }, '40%': { boxShadow: '0 0 0 14px var(--pulse-color)', opacity: 0 }, - '42%': { boxShadow: '0 0 0 2px var(--pulse-color)', opacity: 0 }, + '42%': { boxShadow: '0 0 0 -2px var(--pulse-color)', opacity: 0 }, /* Pause 1 (42-50%) */ - '50%': { boxShadow: '0 0 0 2px var(--pulse-color)', opacity: 0 }, + '50%': { boxShadow: '0 0 0 -2px var(--pulse-color)', opacity: 0 }, /* Pulse 2 (50-92%) */ - '52%': { boxShadow: '0 0 0 2px var(--pulse-color)', opacity: 1 }, + '52%': { boxShadow: '0 0 0 -2px var(--pulse-color)', opacity: 1 }, '90%': { boxShadow: '0 0 0 14px var(--pulse-color)', opacity: 0 }, - '92%': { boxShadow: '0 0 0 2px var(--pulse-color)', opacity: 0 }, + '92%': { boxShadow: '0 0 0 -2px var(--pulse-color)', opacity: 0 }, /* Pause 2 (92-100%) - same length as Pause 1, color change at iteration */ - '100%': { boxShadow: '0 0 0 2px var(--pulse-color)', opacity: 0 }, + '100%': { boxShadow: '0 0 0 -2px var(--pulse-color)', opacity: 0 }, }, }));