Trim flow chevrons to fit one row, add water-passing indicator

Regen flow indicators now show 2 chevrons instead of 3 so the diagram row
fits without wrapping on narrow cards. Added a pulsing vertical chevron
stack under each tank's % reading, shown when that tank is actually
in-service and passing water (not regenerating).
This commit is contained in:
jtricerolph 2026-07-30 09:46:13 +00:00
parent 0ab98d3a3c
commit ff145957fc
2 changed files with 30 additions and 5 deletions

View file

@ -1,4 +1,4 @@
import { RefreshCw, ChevronsRight, ChevronsLeft } from 'lucide-react' import { RefreshCw, ChevronsRight, ChevronsLeft, ChevronsDown } from 'lucide-react'
import type { AssetStatus, TelemetryField } from '../types' import type { AssetStatus, TelemetryField } from '../types'
function fieldByKey(latest: TelemetryField[], key: string | null): TelemetryField | undefined { function fieldByKey(latest: TelemetryField[], key: string | null): TelemetryField | undefined {
@ -44,6 +44,10 @@ function Tank({ label, pct, valueLabel, variant, regenerating }: {
regenerating: boolean regenerating: boolean
}) { }) {
const clamped = pct === null ? 0 : Math.max(0, Math.min(100, pct)) const clamped = pct === null ? 0 : Math.max(0, Math.min(100, pct))
// Water is passing through whichever tank is actually in service right now
// (or the single tank, when it's not offline regenerating) — shown as
// pulsing down-chevrons under the % reading.
const passingWater = !regenerating && variant !== 'standby'
return ( return (
<div className={`softener-tank softener-tank-${variant}`}> <div className={`softener-tank softener-tank-${variant}`}>
{variant === 'active' && ( {variant === 'active' && (
@ -54,7 +58,15 @@ function Tank({ label, pct, valueLabel, variant, regenerating }: {
)} )}
<div className="softener-tank-shell"> <div className="softener-tank-shell">
<div className={`softener-tank-fill ${levelClass(pct)}`} style={{ height: `${clamped}%` }} /> <div className={`softener-tank-fill ${levelClass(pct)}`} style={{ height: `${clamped}%` }} />
<div className="softener-tank-pct">{pct === null ? '—' : `${Math.round(pct)}%`}</div> <div className="softener-tank-content">
<div className="softener-tank-pct">{pct === null ? '—' : `${Math.round(pct)}%`}</div>
{passingWater && (
<div className="softener-tank-water" aria-hidden="true">
<ChevronsDown size={11} strokeWidth={2} />
<ChevronsDown size={11} strokeWidth={2} />
</div>
)}
</div>
</div> </div>
<div className="softener-tank-label">{label}</div> <div className="softener-tank-label">{label}</div>
<div className="softener-tank-value">{valueLabel}</div> <div className="softener-tank-value">{valueLabel}</div>
@ -105,7 +117,6 @@ export default function WaterSoftenerDiagram({ asset }: { asset: AssetStatus })
<div className={`softener-flow softener-flow-${direction} ${active ? 'softener-flow-active' : ''}`} aria-hidden="true"> <div className={`softener-flow softener-flow-${direction} ${active ? 'softener-flow-active' : ''}`} aria-hidden="true">
<Icon size={16} strokeWidth={2} /> <Icon size={16} strokeWidth={2} />
<Icon size={16} strokeWidth={2} /> <Icon size={16} strokeWidth={2} />
<Icon size={16} strokeWidth={2} />
</div> </div>
) )
} }

View file

@ -249,7 +249,6 @@ html, body, #root { height: 100%; margin: 0; font-size: 14px; }
.softener-flow-left svg { margin-right: -6px; } .softener-flow-left svg { margin-right: -6px; }
.softener-flow-active svg { color: var(--app-primary); animation: softener-flow-pulse 1.1s ease-in-out infinite; } .softener-flow-active svg { color: var(--app-primary); animation: softener-flow-pulse 1.1s ease-in-out infinite; }
.softener-flow-active svg:nth-child(2) { animation-delay: .15s; } .softener-flow-active svg:nth-child(2) { animation-delay: .15s; }
.softener-flow-active svg:nth-child(3) { animation-delay: .3s; }
@keyframes softener-flow-pulse { @keyframes softener-flow-pulse {
0%, 100% { opacity: .25; } 0%, 100% { opacity: .25; }
50% { opacity: 1; } 50% { opacity: 1; }
@ -304,14 +303,29 @@ html, body, #root { height: 100%; margin: 0; font-size: 14px; }
.softener-fill-warning { background: var(--sev-warning); } .softener-fill-warning { background: var(--sev-warning); }
.softener-fill-critical { background: var(--sev-critical); } .softener-fill-critical { background: var(--sev-critical); }
.softener-fill-unknown { background: var(--card-border); height: 0 !important; } .softener-fill-unknown { background: var(--card-border); height: 0 !important; }
.softener-tank-pct { .softener-tank-content {
position: relative; position: relative;
z-index: 1; z-index: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: 2px;
}
.softener-tank-pct {
font-size: 12px; font-size: 12px;
font-weight: 700; font-weight: 700;
color: var(--text-dark); color: var(--text-dark);
text-shadow: 0 1px 2px rgba(255,255,255,.6); text-shadow: 0 1px 2px rgba(255,255,255,.6);
} }
.softener-tank-water {
display: flex;
flex-direction: column;
align-items: center;
color: var(--app-primary);
filter: drop-shadow(0 1px 1px rgba(255,255,255,.6));
}
.softener-tank-water svg { margin-top: -4px; animation: softener-flow-pulse 1.1s ease-in-out infinite; }
.softener-tank-water svg:nth-child(2) { animation-delay: .2s; }
.softener-tank-label { font-size: 11px; font-weight: 600; color: var(--text-dark); margin-top: 6px; } .softener-tank-label { font-size: 11px; font-weight: 600; color: var(--text-dark); margin-top: 6px; }
.softener-tank-value { font-size: 10px; color: var(--text-mid); } .softener-tank-value { font-size: 10px; color: var(--text-mid); }