feat collapse graph controls on mobile

- wrap shared graph controls in an accessible mobile disclosure - add a directional chevron that reflects the expanded state - group timeframe and smoothing controls on one uniform menu surface - preserve always-visible controls on desktop clients - cover the disclosure contract in the probe page test

Commit
004cf6b4e1313b3a5b4273695ee7b2a3a66979c3
Author
GPT-5 Codex high <codex@openai.com>
Author date
Committer
GPT-5 Codex high <codex@openai.com>
Committer date
Changed files
roles/dashboard/public/css/app.css
index 1a7258cb..29abd776 100644..100644
@@ -679,6 +679,16 @@
679 679 margin: 0;
680 680 }
681 681
682 Added: .graph-controls-disclosure,
683 Added: .graph-controls-panel {
684 Added: display: flex;
685 Added: align-items: center;
686 Added: }
687 Added:
688 Added: .graph-controls-toggle {
689 Added: display: none;
690 Added: }
691 Added:
682 692 .graph-control .timeframe-option {
683 693 display: flex;
684 694 min-width: 4.75rem;
@@ -975,6 +985,67 @@
975 985 .toolbar-items {
976 986 width: 100%;
977 987 margin-left: 0;
988 Added: }
989 Added:
990 Added: .graph-controls-disclosure {
991 Added: display: block;
992 Added: width: 100%;
993 Added: }
994 Added:
995 Added: .graph-controls-toggle {
996 Added: display: flex;
997 Added: align-items: center;
998 Added: justify-content: space-between;
999 Added: width: 100%;
1000 Added: min-height: var(--toolbar-item-height);
1001 Added: border: 1px solid var(--border);
1002 Added: border-radius: 0.5rem;
1003 Added: padding: 0.5rem 0.75rem;
1004 Added: background: white;
1005 Added: color: var(--text);
1006 Added: cursor: pointer;
1007 Added: font: inherit;
1008 Added: font-weight: 700;
1009 Added: }
1010 Added:
1011 Added: .graph-controls-toggle::after {
1012 Added: width: 0.55rem;
1013 Added: height: 0.55rem;
1014 Added: margin: 0 0.2rem 0.25rem 0.75rem;
1015 Added: border-right: 2px solid currentColor;
1016 Added: border-bottom: 2px solid currentColor;
1017 Added: content: "";
1018 Added: transform: rotate(45deg);
1019 Added: transition: transform 0.15s ease;
1020 Added: }
1021 Added:
1022 Added: .graph-controls-toggle[aria-expanded="true"]::after {
1023 Added: margin-top: 0.25rem;
1024 Added: margin-bottom: 0;
1025 Added: transform: rotate(225deg);
1026 Added: }
1027 Added:
1028 Added: .graph-controls-toggle:hover {
1029 Added: border-color: var(--accent);
1030 Added: background: var(--accent-soft);
1031 Added: }
1032 Added:
1033 Added: .graph-controls-toggle:focus-visible {
1034 Added: outline: 2px solid var(--accent);
1035 Added: outline-offset: 2px;
1036 Added: }
1037 Added:
1038 Added: .graph-controls-panel {
1039 Added: display: none;
1040 Added: margin-top: 0.5rem;
1041 Added: border: 1px solid var(--border);
1042 Added: border-radius: 0.5rem;
1043 Added: padding: 0.75rem;
1044 Added: background: var(--card);
1045 Added: }
1046 Added:
1047 Added: .graph-controls-toggle[aria-expanded="true"] + .graph-controls-panel {
1048 Added: display: block;
978 1049 }
979 1050
980 1051 .toolbar-items .graph-control {
roles/dashboard/public/js/dashboard.js
index b3248dc3..b78a9071 100644..100644
@@ -130,6 +130,10 @@
130 130 return Array.from(document.querySelectorAll("[data-raw-download]"));
131 131 }
132 132
133 Added: function graphControlsToggles() {
134 Added: return Array.from(document.querySelectorAll("[data-graph-controls-toggle]"));
135 Added: }
136 Added:
133 137 function labelForRow(row) {
134 138 return formatChartTime(row.timestamp);
135 139 }
@@ -938,6 +942,13 @@
938 942 smoothingToggles().forEach(toggle => {
939 943 toggle.addEventListener("change", () => {
940 944 setSmoothing(toggle.checked);
945 Added: });
946 Added: });
947 Added:
948 Added: graphControlsToggles().forEach(toggle => {
949 Added: toggle.addEventListener("click", () => {
950 Added: const expanded = toggle.getAttribute("aria-expanded") === "true";
951 Added: toggle.setAttribute("aria-expanded", expanded ? "false" : "true");
941 952 });
942 953 });
943 954
roles/dashboard/t/01-graph-page.t
index de412346..890dccfa 100644..100644
@@ -22,6 +22,10 @@
22 22 ->element_exists(
23 23 'div.toolbar-items div.graph-control > div.timeframe-control[role="group"]'
24 24 )
25 Added: ->text_is(
26 Added: 'button[data-graph-controls-toggle][aria-expanded="false"][aria-controls="graph-controls-panel"]',
27 Added: 'Graph controls'
28 Added: )
25 29 ->element_exists('input[data-smoothing-toggle][checked]')
26 30 ->text_is( 'a[data-raw-download][data-probe="ph"]', 'Download raw data' )
27 31 ->element_exists('tbody[data-reading-messages="ph"]')
roles/dashboard/templates/dashboard/graph-control.html.ep
index c4696a81..75c8d0d2 100644..100644
@@ -1,31 +1,41 @@
1 1 <!-- -*- mode: web; -*- -->
2 2
3 Removed: <div class="graph-control">
4 Removed: <div class="timeframe-control" role="group" aria-label="<%= $aria_label %>">
5 Removed: % for my $timeframe ( [ hour => 'Hourly' ], [ day => 'Daily' ], [ week => 'Weekly' ], [ month => 'Monthly' ], [ year => 'Yearly' ] ) {
6 Removed: <div class="timeframe-option" data-timeframe-option="<%= $timeframe->[0] %>">
7 Removed: <button
8 Removed: class="timeframe-adjustment"
9 Removed: type="button"
10 Removed: data-timeframe-adjustment="decrease"
11 Removed: data-timeframe="<%= $timeframe->[0] %>"
12 Removed: aria-label="Decrease <%= $timeframe->[0] %> range">−</button>
13 Removed: <button
14 Removed: class="timeframe-button<%= $timeframe->[0] eq 'day' ? ' is-active' : '' %>"
15 Removed: type="button"
16 Removed: data-timeframe="<%= $timeframe->[0] %>"
17 Removed: data-timeframe-label="<%= $timeframe->[1] %>"><%= $timeframe->[1] %></button>
18 Removed: <button
19 Removed: class="timeframe-adjustment"
20 Removed: type="button"
21 Removed: data-timeframe-adjustment="increase"
22 Removed: data-timeframe="<%= $timeframe->[0] %>"
23 Removed: aria-label="Increase <%= $timeframe->[0] %> range">+</button>
3 Added: <div class="graph-controls-disclosure">
4 Added: <button
5 Added: class="graph-controls-toggle"
6 Added: type="button"
7 Added: aria-expanded="false"
8 Added: aria-controls="graph-controls-panel"
9 Added: data-graph-controls-toggle>Graph controls</button>
10 Added: <div class="graph-controls-panel" id="graph-controls-panel">
11 Added: <div class="graph-control">
12 Added: <div class="timeframe-control" role="group" aria-label="<%= $aria_label %>">
13 Added: % for my $timeframe ( [ hour => 'Hourly' ], [ day => 'Daily' ], [ week => 'Weekly' ], [ month => 'Monthly' ], [ year => 'Yearly' ] ) {
14 Added: <div class="timeframe-option" data-timeframe-option="<%= $timeframe->[0] %>">
15 Added: <button
16 Added: class="timeframe-adjustment"
17 Added: type="button"
18 Added: data-timeframe-adjustment="decrease"
19 Added: data-timeframe="<%= $timeframe->[0] %>"
20 Added: aria-label="Decrease <%= $timeframe->[0] %> range">−</button>
21 Added: <button
22 Added: class="timeframe-button<%= $timeframe->[0] eq 'day' ? ' is-active' : '' %>"
23 Added: type="button"
24 Added: data-timeframe="<%= $timeframe->[0] %>"
25 Added: data-timeframe-label="<%= $timeframe->[1] %>"><%= $timeframe->[1] %></button>
26 Added: <button
27 Added: class="timeframe-adjustment"
28 Added: type="button"
29 Added: data-timeframe-adjustment="increase"
30 Added: data-timeframe="<%= $timeframe->[0] %>"
31 Added: aria-label="Increase <%= $timeframe->[0] %> range">+</button>
32 Added: </div>
33 Added: % }
24 34 </div>
25 Removed: % }
35 Added: <label class="smoothing-control">
36 Added: <input type="checkbox" data-smoothing-toggle checked>
37 Added: <span>Smoothed</span>
38 Added: </label>
39 Added: </div>
26 40 </div>
27 Removed: <label class="smoothing-control">
28 Removed: <input type="checkbox" data-smoothing-toggle checked>
29 Removed: <span>Smoothed</span>
30 Removed: </label>
31 41 </div>