refactor separate range controls from timeframe selectors

Replace the expanding timeframe buttons (click to reveal +/−) with two distinct button groups: 1. Range control (−/label/+) — adjusts the range of the active timeframe 2. Timeframe selector (Hourly/Daily/Weekly/Monthly/Yearly) — selects Clicking a timeframe button no longer triggers expansion. The range label shows the current multiplier (e.g. '3 days') and updates live. - Remove timeframe-option wrapper divs and is-expanded CSS - Simplify timeframe.js (remove expanded state) - graph-controls.js now binds range adjustment to the active timeframe

Commit
5c8fa8c480075b58cde3fd92cdea7ee7b48310d3
Author
GPT-5 medium <codex@openai.com>
Author date
Committer
GPT-5 medium <codex@openai.com>
Committer date
Changed files
roles/dashboard/public/css/app.css
index 52a9069d..5b25e84d 100644..100644
@@ -239,21 +239,9 @@
239 239 }
240 240
241 241 /* ----------------------------------------------------------------
242 Removed: Timeframe control adjustments: hide +/− by default,
243 Removed: show when option is expanded (JS adds is-expanded)
242 Added: Timeframe control active state
244 243 ---------------------------------------------------------------- */
245 Removed: .timeframe-adjustment {
246 Removed: display: none;
247 Removed: }
248 Removed:
249 Removed: .timeframe-option.is-expanded .timeframe-adjustment {
250 Removed: display: inline-flex;
251 Removed: align-items: center;
252 Removed: justify-content: center;
253 Removed: }
254 Removed:
255 244 .timeframe-button.is-active {
256 Removed: /* JS toggles this; override Bootstrap outline style */
257 245 background-color: var(--fapg-accent) !important;
258 246 border-color: var(--fapg-accent) !important;
259 247 color: #fff !important;
roles/dashboard/public/js/dashboard/graph-controls.js
index ec5b8b85..1a68d769 100644..100644
@@ -1,4 +1,4 @@
1 Removed: import { MAX_TIMEFRAME_RANGE, TIMEFRAMES } from "./constants.js";
1 Added: import { MAX_TIMEFRAME_RANGE } from "./constants.js";
2 2 import { timeframeRangeLabel } from "./timeframe.js";
3 3
4 4 function updateRawDownloadLinks(timeframe) {
@@ -13,40 +13,44 @@
13 13 }
14 14
15 15 function updateControls(timeframe) {
16 Removed: document.querySelectorAll("[data-timeframe-option]").forEach(option => {
17 Removed: const value = option.dataset.timeframeOption;
18 Removed: const range = timeframe.ranges[value] || 1;
19 Removed: const expanded = value === timeframe.expanded();
20 Removed: const button = option.querySelector(".timeframe-button");
16 Added: const range = timeframe.range();
21 17
22 Removed: option.classList.toggle("is-active", value === timeframe.selected());
23 Removed: option.classList.toggle("is-expanded", expanded);
24 Removed: button.classList.toggle("is-active", value === timeframe.selected());
25 Removed: button.textContent = expanded ? timeframeRangeLabel(value, range) : button.dataset.timeframeLabel;
26 Removed: option.querySelector('[data-timeframe-adjustment="decrease"]').disabled = range <= 1;
27 Removed: option.querySelector('[data-timeframe-adjustment="increase"]').disabled = range >= MAX_TIMEFRAME_RANGE;
28 Removed: });
29 Removed:
30 18 document.querySelectorAll(".timeframe-button").forEach(button => {
31 19 button.classList.toggle("is-active", button.dataset.timeframe === timeframe.selected());
32 20 });
21 Added:
22 Added: document.querySelectorAll("[data-range-label]").forEach(label => {
23 Added: label.textContent = timeframeRangeLabel(timeframe.selected(), range);
24 Added: });
25 Added:
26 Added: document.querySelectorAll('[data-range-adjustment="decrease"]').forEach(button => {
27 Added: button.disabled = range <= 1;
28 Added: });
29 Added:
30 Added: document.querySelectorAll('[data-range-adjustment="increase"]').forEach(button => {
31 Added: button.disabled = range >= MAX_TIMEFRAME_RANGE;
32 Added: });
33 Added:
33 34 updateRawDownloadLinks(timeframe);
34 35 }
35 36
36 37 export function initialiseGraphControls(timeframe, refreshCharts) {
37 38 document.querySelectorAll(".timeframe-button").forEach(button => {
38 39 button.addEventListener("click", () => {
39 Removed: timeframe.select(button.dataset.timeframe, Boolean(button.closest("[data-timeframe-option]")));
40 Added: timeframe.select(button.dataset.timeframe);
40 41 updateControls(timeframe);
41 42 refreshCharts();
42 43 });
43 44 });
44 Removed: document.querySelectorAll("[data-timeframe-adjustment]").forEach(button => {
45 Added:
46 Added: document.querySelectorAll("[data-range-adjustment]").forEach(button => {
45 47 button.addEventListener("click", () => {
46 Removed: timeframe.adjust(button.dataset.timeframe, button.dataset.timeframeAdjustment === "increase" ? 1 : -1);
48 Added: const direction = button.dataset.rangeAdjustment === "increase" ? 1 : -1;
49 Added: timeframe.adjust(timeframe.selected(), direction);
47 50 updateControls(timeframe);
48 51 refreshCharts();
49 52 });
50 53 });
54 Added:
51 55 updateControls(timeframe);
52 56 }
roles/dashboard/public/js/dashboard/timeframe.js
index f9090373..0969f40d 100644..100644
@@ -2,7 +2,6 @@
2 2
3 3 export function createTimeframeState() {
4 4 let active = DEFAULT_TIMEFRAME;
5 Removed: let expanded = null;
6 5 const ranges = Object.fromEntries(Object.keys(TIMEFRAMES).map(timeframe => [timeframe, 1]));
7 6
8 7 function selected() {
@@ -12,9 +11,8 @@
12 11 return {
13 12 selected,
14 13 range: () => ranges[selected()] || 1,
15 Removed: expanded: () => expanded,
16 14 ranges,
17 Removed: select(timeframe, expand = false) {
15 Added: select(timeframe) {
18 16 const next = TIMEFRAMES[timeframe] ? timeframe : DEFAULT_TIMEFRAME;
19 17
20 18 if (next === active) {
@@ -22,13 +20,11 @@
22 20 }
23 21
24 22 active = next;
25 Removed: expanded = expand ? active : null;
26 23 },
27 24 adjust(timeframe, adjustment) {
28 25 if (!TIMEFRAMES[timeframe]) return;
29 26
30 27 active = timeframe;
31 Removed: expanded = timeframe;
32 28 ranges[timeframe] = Math.min(
33 29 MAX_TIMEFRAME_RANGE,
34 30 Math.max(1, (ranges[timeframe] || 1) + adjustment)
roles/dashboard/t/01-graph-page.t
index 3efe9a4d..7c7ef5d8 100644..100644
@@ -24,7 +24,7 @@
24 24 ->element_exists('section#probe-ph[data-panel="ph"] canvas#chart-ph')
25 25 ->element_exists_not('[data-status="ph"]')
26 26 ->element_exists(
27 Removed: 'div.toolbar-items div.graph-control.timeframe-control[role="group"]'
27 Added: 'div.toolbar-items div.graph-control .timeframe-control[role="group"]'
28 28 )
29 29 ->text_is(
30 30 'button[data-graph-controls-toggle][aria-expanded="false"][aria-controls="graph-controls-panel"]',
@@ -44,7 +44,7 @@
44 44
45 45 for my $timeframe (qw(hour day week month year)) {
46 46 $t->element_exists(
47 Removed: qq{[data-timeframe-option="$timeframe"] [data-timeframe="$timeframe"]}
47 Added: qq{.timeframe-control button[data-timeframe="$timeframe"]}
48 48 );
49 49 }
50 50
roles/dashboard/templates/dashboard/graph-control.html.ep
index 8d3a68ea..864418cd 100644..100644
@@ -10,28 +10,34 @@
10 10 aria-controls="graph-controls-panel"
11 11 data-graph-controls-toggle>Graph controls</button>
12 12 <div class="collapse d-md-block graph-controls-panel" id="graph-controls-panel">
13 Removed: <div class="graph-control btn-group timeframe-control" role="group" aria-label="<%= $aria_label %>">
14 Removed: % for my $timeframe ( [ hour => 'Hourly' ], [ day => 'Daily' ], [ week => 'Weekly' ], [ month => 'Monthly' ], [ year => 'Yearly' ] ) {
15 Removed: <div class="timeframe-option d-inline-flex" data-timeframe-option="<%= $timeframe->[0] %>">
16 Removed: <button
17 Removed: class="btn btn-sm btn-outline-secondary timeframe-adjustment"
18 Removed: type="button"
19 Removed: data-timeframe-adjustment="decrease"
20 Removed: data-timeframe="<%= $timeframe->[0] %>"
21 Removed: aria-label="Decrease <%= $timeframe->[0] %> range">−</button>
22 Removed: <button
23 Removed: class="btn btn-sm btn-outline-secondary timeframe-button"
24 Removed: type="button"
25 Removed: data-timeframe="<%= $timeframe->[0] %>"
26 Removed: data-timeframe-label="<%= $timeframe->[1] %>"><%= $timeframe->[1] %></button>
27 Removed: <button
28 Removed: class="btn btn-sm btn-outline-secondary timeframe-adjustment"
29 Removed: type="button"
30 Removed: data-timeframe-adjustment="increase"
31 Removed: data-timeframe="<%= $timeframe->[0] %>"
32 Removed: aria-label="Increase <%= $timeframe->[0] %> range">+</button>
33 Removed: </div>
34 Removed: % }
13 Added: <div class="graph-control d-inline-flex gap-2" aria-label="<%= $aria_label %>">
14 Added: <div class="btn-group range-control" role="group" aria-label="Adjust range">
15 Added: <button
16 Added: class="btn btn-sm btn-outline-secondary"
17 Added: type="button"
18 Added: data-range-adjustment="decrease"
19 Added: aria-label="Decrease range">−</button>
20 Added: <button
21 Added: class="btn btn-sm btn-outline-secondary range-label"
22 Added: type="button"
23 Added: data-range-label
24 Added: disabled
25 Added: aria-live="polite">1</button>
26 Added: <button
27 Added: class="btn btn-sm btn-outline-secondary"
28 Added: type="button"
29 Added: data-range-adjustment="increase"
30 Added: aria-label="Increase range">+</button>
31 Added: </div>
32 Added: <div class="btn-group timeframe-control" role="group" aria-label="Select timeframe">
33 Added: % for my $timeframe ( [ hour => 'Hourly' ], [ day => 'Daily' ], [ week => 'Weekly' ], [ month => 'Monthly' ], [ year => 'Yearly' ] ) {
34 Added: <button
35 Added: class="btn btn-sm btn-outline-secondary timeframe-button"
36 Added: type="button"
37 Added: data-timeframe="<%= $timeframe->[0] %>"
38 Added: data-timeframe-label="<%= $timeframe->[1] %>"><%= $timeframe->[1] %></button>
39 Added: % }
40 Added: </div>
35 41 </div>
36 42 </div>
37 43 </div>