Skip to content

Commit 1c480d7

Browse files
committed
Add multi-target highlighting and tour UI
Switch accumulator highlighting from a single target to a list (SingleHighlightTarget[]), updating AccumulatorContext and consumers (CellGrid, ThermistorBar, MasterAlertPanel) to handle multiple simultaneous highlights and preserve auto-clear behavior. MasterAlertPanel now supports clicking the CELL Δ stat to highlight both min/max cells, StatCard and ModuleCard accept optional id props, and highlight checks use array.some. UI tweaks: refactor bottom banner layout and z-index, disable chart line animations in ChargingCurve, and add a TourGuide with predefined steps plus floating tour buttons and element ids in Accumulator and Dashboard for guided onboarding. Also includes minor formatting/structure cleanups in Dashboard and other components.
1 parent 83ed2ca commit 1c480d7

9 files changed

Lines changed: 231 additions & 173 deletions

File tree

pecan/src/components/Banners.tsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -66,17 +66,16 @@ function DefaultBanner({ open, onClose, onOpenSettings }: Readonly<InputProps>)
6666
};
6767

6868
return (
69-
<div className="fixed bottom-0 left-0 right-0 z-50 flex flex-row w-full bg-dropdown-menu-bg justify-between items-center box-border px-4 py-3 shadow-lg border-t border-gray-600">
70-
<div className="w-[10%]"></div>
71-
<div className="flex-1 flex justify-center items-center gap-2">
69+
<div className="fixed bottom-0 left-0 right-0 z-30 flex flex-row w-full bg-dropdown-menu-bg justify-center items-center box-border px-4 py-3 shadow-lg border-t border-gray-600 gap-8">
70+
<div className="flex items-center gap-2">
7271
<span className="text-white text-[14pt] font-semibold text-center whitespace-nowrap overflow-hidden text-ellipsis">
7372
Using preconfigured DBC file.
7473
<span className="text-gray-300 ml-2 font-normal">
7574
(Dismissing in {timeLeft}s)
7675
</span>
7776
</span>
7877
</div>
79-
<div className="flex flex-row items-center justify-end gap-4 pe-4 shrink-0">
78+
<div className="flex flex-row items-center gap-4 shrink-0">
8079
<button
8180
onClick={handleOpenSettings}
8281
className="bg-banner-button hover:bg-banner-button-hover px-4 py-2 cursor-pointer text-center text-[12pt] font-semibold text-white rounded-md transition-colors shadow-sm whitespace-nowrap"

pecan/src/components/accumulator/AccumulatorContext.tsx

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,40 +8,43 @@
88
import { createContext, useContext, useState, useCallback, type ReactNode } from 'react';
99
import type { ModuleId } from './AccumulatorTypes';
1010

11-
export type HighlightTarget = {
11+
export type SingleHighlightTarget = {
1212
moduleId: ModuleId;
1313
type: 'cell' | 'thermistor';
1414
index: number;
15-
} | null;
15+
};
16+
17+
export type HighlightTarget = SingleHighlightTarget | SingleHighlightTarget[] | null;
1618

1719
interface AccumulatorContextType {
18-
highlightTarget: HighlightTarget;
20+
highlightTargets: SingleHighlightTarget[];
1921
setHighlightTarget: (target: HighlightTarget) => void;
2022
clearHighlight: () => void;
2123
}
2224

2325
const AccumulatorContext = createContext<AccumulatorContextType | null>(null);
2426

2527
export function AccumulatorProvider({ children }: { children: ReactNode }) {
26-
const [highlightTarget, setHighlightTargetState] = useState<HighlightTarget>(null);
28+
const [highlightTargets, setHighlightTargets] = useState<SingleHighlightTarget[]>([]);
2729

2830
const setHighlightTarget = useCallback((target: HighlightTarget) => {
29-
setHighlightTargetState(target);
31+
const targets = Array.isArray(target) ? target : target ? [target] : [];
32+
setHighlightTargets(targets);
3033

3134
// Auto-clear highlight after 2 seconds (after blink animation)
3235
if (target) {
3336
setTimeout(() => {
34-
setHighlightTargetState(null);
37+
setHighlightTargets([]);
3538
}, 2000);
3639
}
3740
}, []);
3841

3942
const clearHighlight = useCallback(() => {
40-
setHighlightTargetState(null);
43+
setHighlightTargets([]);
4144
}, []);
4245

4346
return (
44-
<AccumulatorContext.Provider value={{ highlightTarget, setHighlightTarget, clearHighlight }}>
47+
<AccumulatorContext.Provider value={{ highlightTargets, setHighlightTarget, clearHighlight }}>
4548
{children}
4649
</AccumulatorContext.Provider>
4750
);

pecan/src/components/accumulator/CellGrid.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ function Cell({ moduleId, cellIndex, stats, onClick }: {
3535
stats: CellStats;
3636
onClick?: () => void;
3737
}) {
38-
const { highlightTarget } = useAccumulatorContext();
38+
const { highlightTargets } = useAccumulatorContext();
3939
const cellRef = useRef<HTMLDivElement>(null);
4040
const { signalName } = getCellSignalInfo(moduleId, cellIndex);
4141

@@ -53,9 +53,9 @@ function Cell({ moduleId, cellIndex, stats, onClick }: {
5353
);
5454

5555
// Check if this cell is highlighted
56-
const isHighlighted = highlightTarget?.type === 'cell' &&
57-
highlightTarget?.moduleId === moduleId &&
58-
highlightTarget?.index === cellIndex;
56+
const isHighlighted = highlightTargets.some(t =>
57+
t.type === 'cell' && t.moduleId === moduleId && t.index === cellIndex
58+
);
5959

6060
// Scroll into view when highlighted
6161
useEffect(() => {

pecan/src/components/accumulator/ChargingCurve.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,7 @@ export default function ChargingCurve({
194194
strokeWidth={2}
195195
dot={false}
196196
connectNulls
197+
isAnimationActive={false}
197198
/>
198199
<Line
199200
yAxisId="temp"
@@ -203,6 +204,7 @@ export default function ChargingCurve({
203204
strokeWidth={2}
204205
dot={false}
205206
connectNulls
207+
isAnimationActive={false}
206208
/>
207209
</LineChart>
208210
</ResponsiveContainer>

pecan/src/components/accumulator/MasterAlertPanel.tsx

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -335,14 +335,16 @@ export default function MasterAlertPanel() {
335335
? '#f97316' : '#22c55e';
336336

337337
// Stat card component for mobile-friendly touch targets
338-
const StatCard = ({ label, value, sublabel, color, onClick }: {
338+
const StatCard = ({ label, value, sublabel, color, onClick, id }: {
339339
label: string;
340340
value: string;
341341
sublabel?: string;
342342
color: string;
343343
onClick?: () => void;
344+
id?: string;
344345
}) => (
345346
<button
347+
id={id}
346348
onClick={onClick}
347349
disabled={!onClick}
348350
className={`
@@ -360,9 +362,10 @@ export default function MasterAlertPanel() {
360362
</button>
361363
);
362364

363-
const handleCellClick = (stat: { moduleId: ModuleId; index: number } | null) => {
364-
if (stat) {
365-
setHighlightTarget({ moduleId: stat.moduleId, index: stat.index, type: 'cell' });
365+
const handleCellClick = (stats: { moduleId: ModuleId; index: number }[] | { moduleId: ModuleId; index: number } | null) => {
366+
if (stats) {
367+
const targets = Array.isArray(stats) ? stats : [stats];
368+
setHighlightTarget(targets.map(s => ({ moduleId: s.moduleId, index: s.index, type: 'cell' })));
366369
}
367370
};
368371

@@ -392,13 +395,14 @@ export default function MasterAlertPanel() {
392395

393396
{/* Cell Delta */}
394397
<StatCard
398+
id="accu-delta-stat"
395399
label="CELL Δ"
396400
value={packStats.cellDelta !== null ? `${(packStats.cellDelta * 1000).toFixed(0)}mV` : '--'}
397401
sublabel={packStats.minVoltage && packStats.maxVoltage
398402
? `${packStats.minVoltage.label}${packStats.maxVoltage.label}`
399403
: undefined}
400404
color={deltaColor}
401-
onClick={() => handleCellClick(packStats.minVoltage)}
405+
onClick={() => handleCellClick([packStats.minVoltage, packStats.maxVoltage].filter((x): x is NonNullable<typeof x> => x !== null))}
402406
/>
403407

404408
{/* Average Voltage */}

pecan/src/components/accumulator/ModuleCard.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ interface ModuleCardProps {
2525
moduleId: ModuleId;
2626
initialOpen?: boolean;
2727
onCellClick?: (moduleId: ModuleId, cellIndex: number) => void;
28+
id?: string;
2829
}
2930

3031
interface ModuleStats {
@@ -123,7 +124,7 @@ function useModuleStats(moduleId: ModuleId): ModuleStats {
123124
return stats;
124125
}
125126

126-
export default function ModuleCard({ moduleId, initialOpen = false, onCellClick }: ModuleCardProps) {
127+
export default function ModuleCard({ moduleId, initialOpen = false, onCellClick, id }: ModuleCardProps) {
127128
const [isOpen, setIsOpen] = useState(initialOpen);
128129
const { voltageStats, tempStats, alertLevel } = useModuleStats(moduleId);
129130

@@ -132,6 +133,7 @@ export default function ModuleCard({ moduleId, initialOpen = false, onCellClick
132133

133134
return (
134135
<div
136+
id={id}
135137
className={`
136138
bg-data-module-bg rounded-lg overflow-hidden
137139
border-2 transition-all duration-300

pecan/src/components/accumulator/ThermistorBar.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,17 +34,17 @@ function ThermistorSegment({ moduleId, thermistorIndex, temp }: {
3434
thermistorIndex: number;
3535
temp: number | null;
3636
}) {
37-
const { highlightTarget } = useAccumulatorContext();
37+
const { highlightTargets } = useAccumulatorContext();
3838
const { signalName } = getThermistorSignalInfo(moduleId, thermistorIndex);
3939

4040
const bgColor = getTemperatureColor(temp);
4141
const isCritical = temp !== null && temp >= ALERT_THRESHOLDS.overTemp.critical;
4242
const isWarning = temp !== null && temp >= ALERT_THRESHOLDS.overTemp.warning;
4343

4444
// Check if this thermistor is highlighted
45-
const isHighlighted = highlightTarget?.type === 'thermistor' &&
46-
highlightTarget?.moduleId === moduleId &&
47-
highlightTarget?.index === thermistorIndex;
45+
const isHighlighted = highlightTargets.some(t =>
46+
t.type === 'thermistor' && t.moduleId === moduleId && t.index === thermistorIndex
47+
);
4848

4949
return (
5050
<div

pecan/src/pages/Accumulator.tsx

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,28 @@ import {
1818
import DraggablePlot from '../components/DraggablePlot';
1919
import { type PlotSignal } from '../components/PlotManager';
2020
import { dataStore } from '../lib/DataStore';
21+
import TourGuide, { type TourStep } from '../components/TourGuide';
22+
23+
const TOUR_STEPS: TourStep[] = [
24+
{
25+
targetId: "accu-module-cards",
26+
title: "Module Grid",
27+
content: "Cells are clickable! Click any cell to open a detailed voltage plot in a floating window.",
28+
position: "right"
29+
},
30+
{
31+
targetId: "accu-chart-window",
32+
title: "Charging Curve",
33+
content: "This chart shows pack-wide trends. You can adjust the time window using the dropdown in the top right.",
34+
position: "left"
35+
},
36+
{
37+
targetId: "accu-delta-stat",
38+
title: "Cell Delta",
39+
content: "Clicking the Delta stat will highlight the two cells with the largest voltage difference in the grid below.",
40+
position: "right"
41+
}
42+
];
2143

2244
export default function Accumulator() {
2345
// Time window for charging curve (5 minutes default)
@@ -26,6 +48,10 @@ export default function Accumulator() {
2648
// Plot modal state
2749
const [plotSignal, setPlotSignal] = useState<PlotSignal | null>(null);
2850

51+
// Tour state
52+
const [tourOpen, setTourOpen] = useState(false);
53+
const [currentTourStep, setCurrentTourStep] = useState(0);
54+
2955
const handleCellClick = (moduleId: ModuleId, cellIndex: number) => {
3056
const { msgId, signalName } = getCellSignalInfo(moduleId, cellIndex);
3157

@@ -63,7 +89,10 @@ export default function Accumulator() {
6389
<ChargingCurve timeWindowMs={chartTimeWindow} />
6490

6591
{/* Time window selector */}
66-
<div className="absolute top-3 right-3 flex items-center gap-2">
92+
<div
93+
id="accu-chart-window"
94+
className="absolute top-3 right-3 flex items-center gap-2"
95+
>
6796
<span className="text-xs text-gray-400">Window:</span>
6897
<select
6998
value={chartTimeWindow}
@@ -81,9 +110,10 @@ export default function Accumulator() {
81110

82111
{/* Module Cards Grid */}
83112
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
84-
{MODULE_IDS.map((moduleId) => (
113+
{MODULE_IDS.map((moduleId, index) => (
85114
<ModuleCard
86115
key={moduleId}
116+
id={index === 0 ? "accu-module-cards" : undefined}
87117
moduleId={moduleId}
88118
initialOpen={true}
89119
onCellClick={handleCellClick}
@@ -100,12 +130,30 @@ export default function Accumulator() {
100130
</div>
101131
</div>
102132

133+
{/* Floating Tour Button */}
134+
<button
135+
onClick={() => setTourOpen(true)}
136+
className="fixed bottom-6 right-6 z-40 w-10 h-10 rounded-full bg-blue-600 text-white shadow-lg flex items-center justify-center hover:bg-blue-500 hover:scale-110 transition-all"
137+
title="Start Tour"
138+
aria-label="Start Tour"
139+
>
140+
<span className="text-lg font-bold">?</span>
141+
</button>
142+
103143
{/* Cell Plot Window (Draggable) */}
104144
<DraggablePlot
105145
isOpen={!!plotSignal}
106146
onClose={() => setPlotSignal(null)}
107147
signalInfo={plotSignal}
108148
/>
149+
150+
<TourGuide
151+
steps={TOUR_STEPS}
152+
isOpen={tourOpen}
153+
onClose={() => setTourOpen(false)}
154+
currentStepIndex={currentTourStep}
155+
onStepChange={setCurrentTourStep}
156+
/>
109157
</div>
110158
</AccumulatorProvider>
111159
);

0 commit comments

Comments
 (0)