merged responsive styling and updated dev proto
This commit is contained in:
commit
f313979655
8 changed files with 106 additions and 888 deletions
|
|
@ -1,57 +1,79 @@
|
|||
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%',
|
||||
border: '2px solid var(--pulse-color)',
|
||||
borderRadius: 'inherit', // Should inherit from Box
|
||||
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',
|
||||
isolation: 'isolate',
|
||||
overflow: 'visible',
|
||||
},
|
||||
pulseRing: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
borderRadius: 'inherit',
|
||||
pointerEvents: 'none',
|
||||
boxSizing: 'border-box',
|
||||
zIndex: -1,
|
||||
animation: '$pulse 4.4s infinite ease-in-out',
|
||||
},
|
||||
'@keyframes pulse': {
|
||||
from: {
|
||||
transform: "scale(1)",
|
||||
opacity: 1
|
||||
},
|
||||
to: {
|
||||
transform: "scale(1.25)",
|
||||
opacity: 0
|
||||
}
|
||||
/* Start behind box, then expand out and fade */
|
||||
/* pulse, pause, pulse, pause - CONSISTENT: both pauses equal, both pulses equal */
|
||||
/* 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 },
|
||||
/* 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<HTMLDivElement, Props>((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 (
|
||||
<Box
|
||||
ref={ref}
|
||||
{...boxProps}
|
||||
className={classNames([classes.pulseWrapper, className])}
|
||||
style={{ '--pulse-color': color } as React.CSSProperties}
|
||||
style={{ '--pulse-color': displayColor } as React.CSSProperties}
|
||||
>
|
||||
<span
|
||||
className={classes.pulseRing}
|
||||
aria-hidden
|
||||
onAnimationIteration={onAnimationIteration}
|
||||
/>
|
||||
{children}
|
||||
</Box>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -16,8 +16,6 @@ const useStyles = makeStyles((theme: Theme) => {
|
|||
},
|
||||
tableContainer: {
|
||||
width: "100%",
|
||||
maxWidth: "100%",
|
||||
overflow: "hidden",
|
||||
minWidth: 0,
|
||||
},
|
||||
title: {
|
||||
|
|
@ -28,13 +26,12 @@ const useStyles = makeStyles((theme: Theme) => {
|
|||
marginTop: theme.spacing(-2),
|
||||
},
|
||||
titleComp: {
|
||||
padding: theme.spacing(1),
|
||||
marginLeft: theme.spacing(-1)
|
||||
padding: theme.spacing(1, 1, 0, 1.5),
|
||||
},
|
||||
subtitleComp: {
|
||||
padding: theme.spacing(1),
|
||||
marginTop: theme.spacing(-2),
|
||||
marginLeft: theme.spacing(-1),
|
||||
padding: theme.spacing(0, 1, 0.5, 1.5),
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
},
|
||||
titleContainer: {
|
||||
border: "none",
|
||||
|
|
@ -42,6 +39,8 @@ const useStyles = makeStyles((theme: Theme) => {
|
|||
searchContainer: {
|
||||
border: "none",
|
||||
textAlign: "right",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
},
|
||||
mobileTitleBox: {
|
||||
margin: theme.spacing(1),
|
||||
|
|
@ -118,8 +117,9 @@ function ResizableHeaderTitle<T>(props: {
|
|||
: {}),
|
||||
}}
|
||||
>
|
||||
<Box component="span" sx={{ whiteSpace: "nowrap" }}>
|
||||
{column.title} {showOrderIcon(column)}
|
||||
<Box component="span" sx={{ whiteSpace: "nowrap", display: "inline-flex", alignItems: "center" }}>
|
||||
<Box component="span">{column.title}</Box>
|
||||
{showOrderIcon(column)}
|
||||
</Box>
|
||||
</Grid2>
|
||||
);
|
||||
|
|
@ -491,9 +491,17 @@ export default function ResponsiveTable<T extends Object>(props: Props<T>) {
|
|||
const sortKey = column.sortKey ? column.sortKey : column.title.toLowerCase()
|
||||
if (sortKey === orderBy) {
|
||||
if (order === "asc") {
|
||||
return <ArrowDownward fontSize="small" sx={{ marginLeft: 1 }} />
|
||||
return (
|
||||
<Box component="span" sx={{ ml: 0.5, display: "inline-flex", alignItems: "center" }}>
|
||||
<ArrowDownward fontSize="small" />
|
||||
</Box>
|
||||
)
|
||||
} else {
|
||||
return <ArrowUpward fontSize="small" sx={{ marginLeft: 1 }} />
|
||||
return (
|
||||
<Box component="span" sx={{ ml: 0.5, display: "inline-flex", alignItems: "center" }}>
|
||||
<ArrowUpward fontSize="small" />
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
}
|
||||
return null;
|
||||
|
|
@ -536,15 +544,13 @@ export default function ResponsiveTable<T extends Object>(props: Props<T>) {
|
|||
const clampedWidth = Math.max(MIN_COLUMN_WIDTH, newWidthForIndex)
|
||||
const actualDelta = newWidths[index] - clampedWidth
|
||||
newWidths[index] = clampedWidth
|
||||
newWidths.every((width, j) => {
|
||||
if (j <= index || width === 0) return true
|
||||
else if (newWidths[j] !== undefined) {
|
||||
newWidths[j] = newWidths[j] + actualDelta
|
||||
return false
|
||||
}
|
||||
})
|
||||
|
||||
if (actualDelta > 0) {
|
||||
// Shrinking current column – give space to next column
|
||||
newWidths[index + 1] = (newWidths[index + 1] ?? 0) + actualDelta
|
||||
}
|
||||
// When growing (actualDelta < 0): never shrink adjacent columns – table grows to accommodate
|
||||
setRowWidths(newWidths)
|
||||
|
||||
}
|
||||
|
||||
const MIN_COLUMN_WIDTH = 24;
|
||||
|
|
@ -553,14 +559,17 @@ export default function ResponsiveTable<T extends Object>(props: Props<T>) {
|
|||
<Box className={classes.tableContainer} component={Paper}>
|
||||
{(title || setSearchText) && (
|
||||
<Box>
|
||||
<Grid2 container justifyContent="space-between" alignItems="center" sx={{ padding: 1 }}>
|
||||
<Grid2
|
||||
container
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
sx={{ px: 1.5, pt: 1, pb: 0.5 }}
|
||||
>
|
||||
<Grid2>
|
||||
<Box sx={{ marginTop: -1 }}>
|
||||
{renderTitle()}
|
||||
{renderSubtitle()}
|
||||
</Box>
|
||||
{renderTitle()}
|
||||
{renderSubtitle()}
|
||||
</Grid2>
|
||||
<Grid2 size={{ xs: "grow" }} sx={{ minWidth: 0, display: "flex", justifyContent: "flex-end" }}>
|
||||
<Grid2 size={{ xs: "grow" }} sx={{ minWidth: 0, display: "flex", justifyContent: "flex-end", mt: -1 }}>
|
||||
<Grid2 container alignItems="center" spacing={1} wrap="nowrap">
|
||||
<Grid2>
|
||||
{actions && actions}
|
||||
|
|
@ -588,7 +597,20 @@ export default function ResponsiveTable<T extends Object>(props: Props<T>) {
|
|||
</Box>
|
||||
)}
|
||||
<TableContainer sx={{ overflowX: "auto", paddingBottom: 1 }}>
|
||||
<Table>
|
||||
<Table
|
||||
sx={{
|
||||
tableLayout: "fixed",
|
||||
width: columns && rowWidths.length > 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%",
|
||||
}}
|
||||
>
|
||||
<TableHead className={stickyHeader ? classes.stickyHeader : undefined}>
|
||||
{customElement &&
|
||||
<TableRow>
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<Tooltip title={tooltip()}>
|
||||
<PulseBox color={color} className={classes.box} >
|
||||
<PulseBox color={colors[0] ?? ""} colors={colors} className={classes.box} >
|
||||
<Grid2 container direction="column" >
|
||||
<Grid2 container direction="column"
|
||||
className={`${classes.carouselItem} ${classes.active }`}
|
||||
|
|
|
|||
|
|
@ -85,23 +85,8 @@ export default function StatusGas(props: Props) {
|
|||
messages = or(device.status[gasType]?.overlays?.map(overlay => 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 (
|
||||
<Tooltip title={tooltip()}>
|
||||
<PulseBox color={color} className={gasType === "all" ? classes.boxTall : classes.box}>
|
||||
<PulseBox color={colors[0] ?? ""} colors={colors} className={gasType === "all" ? classes.boxTall : classes.box}>
|
||||
<Grid2 container direction="column" >
|
||||
{ gasType === "all" &&
|
||||
<Typography variant="body2">
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<Tooltip title={tooltip()}>
|
||||
<PulseBox color={color} className={classes.box} >
|
||||
<PulseBox color={colors[0] ?? ""} colors={colors} className={classes.box} >
|
||||
<Grid2 container direction="column" >
|
||||
<Grid2>
|
||||
<Typography variant="body2" style={{ color: orange[700]}}>
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<Tooltip title={tooltip()}>
|
||||
<PulseBox color={color} className={classes.box} >
|
||||
<PulseBox color={colors[0] ?? ""} colors={colors} className={classes.box} >
|
||||
<Grid2 container direction="column" >
|
||||
<Grid2 container direction="column"
|
||||
className={`${classes.carouselItem} ${
|
||||
|
|
|
|||
|
|
@ -104,28 +104,25 @@ export default function GateDeltaTempGraph(props: Props){
|
|||
*/
|
||||
useEffect(()=>{
|
||||
//get the measurements for the temp component
|
||||
componentAPI.listUnitMeasurements(
|
||||
componentAPI.sampleUnitMeasurements(
|
||||
deviceID,
|
||||
tempComponent.key(),
|
||||
start.toISOString(),
|
||||
end.toISOString(),
|
||||
500,
|
||||
0,
|
||||
"timestamp",
|
||||
500
|
||||
).then(resp => {
|
||||
console.log(resp)
|
||||
if(resp.data.measurements){
|
||||
let tempCompMeasurements = resp.data.measurements.map(um => UnitMeasurement.any(um, user));
|
||||
//if there is an ambient component, then treat the temp component like a single sensor and not a chain, and use the ambient measurements as the inlet
|
||||
if(ambient){
|
||||
//need to then load the measurements for the ambient component
|
||||
componentAPI.listUnitMeasurements(
|
||||
componentAPI.sampleUnitMeasurements(
|
||||
deviceID,
|
||||
ambient.key(),
|
||||
start.toISOString(),
|
||||
end.toISOString(),
|
||||
500,
|
||||
0,
|
||||
"timestamp",
|
||||
500
|
||||
).then(resp => {
|
||||
let ambientMeasurements = resp.data.measurements.map(um => UnitMeasurement.any(um, user));
|
||||
//now loop through each of them to 'combine' them into a single array
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue