[OCaml] High Intensity Training Online
style show progress as per-metric lines with coloured deltas
Render each exercise in a row menu as a bold name, a monospaced weight line, and a monospaced reps line, each with an inline change in parentheses coloured by the app palette: an increase is green, a decrease oxblood. A first performance shows the values with no delta. Replaces the single combined delta line for both native and imported entries.
lib/presenter/pages_logbook.ml
@@ -137,73 +137,57 @@
137
137
];
138
138
]
139
139
140
Removed:
let signed_number n = if n > 0 then Printf.sprintf "+%d" n else string_of_int n
140
Added:
(* An inline coloured delta for one metric: green for an increase, oxblood for a
141
Added:
decrease, following the app's progress palette. [None] when there is no prior
142
Added:
value or the value is unchanged, so no parenthetical is shown. *)
143
Added:
let metric_delta ~previous ~current =
144
Added:
match previous with
145
Added:
| None -> None
146
Added:
| Some p ->
147
Added:
let d = current -. p in
148
Added:
if Float.equal d 0. then None
149
Added:
else
150
Added:
let cls = if d > 0. then "delta-up" else "delta-down" in
151
Added:
Some
152
Added:
(tag "span"
153
Added:
[ Dream_html.string_attr "class" "metric-delta %s" cls ]
154
Added:
[ txt " (%+g)" d ])
141
155
142
Removed:
let change_delta (change : View_model.Logbook_change.t) =
143
Removed:
let load = change.current_load -. change.previous_load in
144
Removed:
let reps = change.current_reps - change.previous_reps in
145
Removed:
let pieces =
146
Removed:
(if Float.equal load 0. then [] else [ Printf.sprintf "%+g kg" load ])
147
Removed:
@
148
Removed:
if reps = 0 then []
149
Removed:
else
150
Removed:
[
151
Removed:
Printf.sprintf "%s rep%s" (signed_number reps)
152
Removed:
(if abs reps = 1 then "" else "s");
153
Removed:
]
154
Removed:
in
155
Removed:
if pieces = [] then "no change" else String.concat ", " pieces
156
Added:
(* One metric line: monospaced "<value> <unit>" with an optional coloured delta.
157
Added:
[unit_] is pluralised by the caller. *)
158
Added:
let metric_line ~value ~unit_ ~delta =
159
Added:
tag "p"
160
Added:
[ class_ "exercise-metric" ]
161
Added:
([ txt "%s %s" value unit_ ] @ Option.to_list delta)
156
162
157
Removed:
(* The colour class for a per-exercise verdict: progress oxblood, regress green,
158
Removed:
stagnation brass. A first performance (no verdict) is neutral. *)
159
Removed:
let verdict_class = function
160
Removed:
| Some View_model.Progressed -> "verdict-progress"
161
Removed:
| Some View_model.Regressed -> "verdict-regress"
162
Removed:
| Some View_model.Stagnant -> "verdict-stagnant"
163
Removed:
| None -> "verdict-none"
164
Removed:
165
Removed:
(* One exercise line in a row menu: name, the efforts, and a colour-coded
166
Removed:
delta. Shared by native and imported menus. *)
167
Removed:
let exercise_line ~name ~efforts ~verdict ~delta =
163
Added:
(* One exercise block in a row menu: a bold name, then a weight line and a reps
164
Added:
line, each with its own coloured change. Shared by native and imported
165
Added:
menus; [prev_*] is [None] for a first performance. *)
166
Added:
let exercise_block ~name ~weight ~reps ~prev_weight ~prev_reps =
168
167
tag "li"
169
Removed:
[ Dream_html.string_attr "class" "%s" (verdict_class verdict) ]
168
Added:
[ class_ "exercise-block" ]
170
169
[
171
170
tag "strong" [] [ txt "%s" name ];
172
Removed:
tag "span" [ class_ "change-efforts" ] [ txt "%s" efforts ];
173
Removed:
tag "span" [ class_ "change-delta" ] [ txt "%s" delta ];
171
Added:
metric_line
172
Added:
~value:(Printf.sprintf "%g" weight)
173
Added:
~unit_:"kg"
174
Added:
~delta:(metric_delta ~previous:prev_weight ~current:weight);
175
Added:
metric_line ~value:(string_of_int reps)
176
Added:
~unit_:(if reps = 1 then "rep" else "reps")
177
Added:
~delta:
178
Added:
(metric_delta
179
Added:
~previous:(Option.map float_of_int prev_reps)
180
Added:
~current:(float_of_int reps));
174
181
]
175
182
176
183
let native_change_line (change : View_model.Logbook_change.t) =
177
Removed:
exercise_line ~name:change.exercise_name
178
Removed:
~efforts:
179
Removed:
(Printf.sprintf "%g kg × %d → %g kg × %d" change.previous_load
180
Removed:
change.previous_reps change.current_load change.current_reps)
181
Removed:
~verdict:(Some change.verdict) ~delta:(change_delta change)
184
Added:
exercise_block ~name:change.exercise_name ~weight:change.current_load
185
Added:
~reps:change.current_reps ~prev_weight:(Some change.previous_load)
186
Added:
~prev_reps:(Some change.previous_reps)
182
187
183
Removed:
(* The progress since an imported effort was last performed, as a compact
184
Removed:
delta. *)
185
Removed:
let imported_delta (e : View_model.Imported_effort.t) =
186
Removed:
match (e.previous_load, e.previous_reps) with
187
Removed:
| Some pl, Some pr ->
188
Removed:
let load = e.load -. pl in
189
Removed:
let reps = e.reps - pr in
190
Removed:
let pieces =
191
Removed:
(if Float.equal load 0. then [] else [ Printf.sprintf "%+g kg" load ])
192
Removed:
@
193
Removed:
if reps = 0 then []
194
Removed:
else
195
Removed:
[
196
Removed:
Printf.sprintf "%s rep%s" (signed_number reps)
197
Removed:
(if abs reps = 1 then "" else "s");
198
Removed:
]
199
Removed:
in
200
Removed:
if pieces = [] then "no change" else String.concat ", " pieces
201
Removed:
| _ -> "first performance"
202
Removed:
203
188
let imported_effort_line (e : View_model.Imported_effort.t) =
204
Removed:
exercise_line ~name:e.exercise_name
205
Removed:
~efforts:(Printf.sprintf "%g kg × %d" e.load e.reps)
206
Removed:
~verdict:e.verdict ~delta:(imported_delta e)
189
Added:
exercise_block ~name:e.exercise_name ~weight:e.load ~reps:e.reps
190
Added:
~prev_weight:e.previous_load ~prev_reps:e.previous_reps
207
191
208
192
(* The three-dot menu shared shape: a summary button opening a drawer. *)
209
193
let row_menu ~aria_summary ~aria_drawer ~extra_class contents =
lib/web/assets/hito.css
@@ -702,33 +702,26 @@
702
702
}
703
703
.logbook-change-list {
704
704
display: grid;
705
Removed:
gap: 0.4rem;
705
Added:
gap: 0.75rem;
706
706
margin: 0;
707
707
padding: 0;
708
708
list-style: none;
709
709
}
710
Removed:
.logbook-change-list li {
711
Removed:
display: grid;
712
Removed:
grid-template-columns: 1fr auto;
713
Removed:
gap: 0.25rem 0.5rem;
714
Removed:
align-items: baseline;
710
Added:
/* One exercise: a bold name, then a weight line and a reps line. */
711
Added:
.exercise-block > strong {
712
Added:
display: block;
713
Added:
color: var(--ink);
715
714
}
716
Removed:
.change-efforts {
717
Removed:
grid-column: 1 / -1;
715
Added:
.exercise-metric {
716
Added:
margin: 0.15rem 0 0;
718
717
font-family: var(--mono);
719
Removed:
white-space: nowrap;
718
Added:
color: var(--ink);
720
719
}
721
Removed:
.change-delta {
722
Removed:
color: var(--oxblood-dark);
723
Removed:
font: 700 var(--font-size-small)/1 var(--sans);
724
Removed:
white-space: nowrap;
725
Removed:
}
726
Removed:
/* Per-exercise progress verdict colours in the row menu: progress green,
727
Removed:
regress oxblood, stagnation brass. The colour lands on that line's delta. */
728
Removed:
.verdict-progress .change-delta { color: var(--evergreen); }
729
Removed:
.verdict-regress .change-delta { color: var(--oxblood-dark); }
730
Removed:
.verdict-stagnant .change-delta { color: var(--brass); }
731
Removed:
.verdict-none .change-delta { color: var(--muted-ink); }
720
Added:
/* The per-metric change, coloured by the app's progress palette: an increase
721
Added:
is green, a decrease oxblood. */
722
Added:
.metric-delta { font-weight: 700; }
723
Added:
.metric-delta.delta-up { color: var(--evergreen); }
724
Added:
.metric-delta.delta-down { color: var(--oxblood-dark); }
732
725
733
726
/* The action bar under the logbook heading. */
734
727
.logbook-actions-bar { margin: 0.5rem 0 1rem; }
test/web/logbook.ml
@@ -103,9 +103,11 @@
103
103
[
104
104
"18 min";
105
105
"Laterals";
106
Removed:
"10 kg × 8 → 12 kg × 9";
107
Removed:
"+2 kg, +1 rep";
108
Removed:
"verdict-progress";
106
Added:
"12 kg";
107
Added:
"(+2)";
108
Added:
"9 reps";
109
Added:
"(+1)";
110
Added:
"delta-up";
109
111
] );
110
112
( "each saved workout row has an edit and confirmation-gated delete menu",
111
113
`Quick,
@@ -522,8 +524,8 @@
522
524
"the imported workout duration is shown" true
523
525
(Option.is_some (index_from ~needle:"60 min" page 0));
524
526
(* An import's status and detail live in a three-dot menu, whose
525
Removed:
drawer lists the imported exercise, its effort, and the progress
526
Removed:
since it was last performed (here a first performance). *)
527
Added:
drawer lists the imported exercise and its effort. This is a first
528
Added:
performance, so no change is shown. *)
527
529
Alcotest.(check bool)
528
530
"the import three-dot menu drawer is present" true
529
531
(Option.is_some
@@ -532,9 +534,13 @@
532
534
"the imported exercise is listed" true
533
535
(Option.is_some (index_from ~needle:"Squats" page 0));
534
536
Alcotest.(check bool)
535
Removed:
"a first performance is marked as such" true
536
Removed:
(Option.is_some (index_from ~needle:"first performance" page 0));
537
Added:
"the effort metrics are shown" true
538
Added:
(Option.is_some (index_from ~needle:"100 kg" page 0)
539
Added:
&& Option.is_some (index_from ~needle:"8 reps" page 0));
537
540
Alcotest.(check bool)
541
Added:
"a first performance shows no change" false
542
Added:
(Option.is_some (index_from ~needle:"metric-delta" page 0));
543
Added:
Alcotest.(check bool)
538
544
"a read-only import offers no delete action" false
539
545
(Option.is_some (index_from ~needle:"?delete=" page 0)) );
540
546
( "an imported exercise shows progress since it was last performed",
@@ -602,10 +608,14 @@
602
608
(Lwt_main.run
603
609
(Memory_repo.imported_records repo trainee.Trainee.id)));
604
610
let page = body (get c "/logbook?year=2024&month=1") in
605
Removed:
(* The later import gained 10 kg over the earlier one. *)
611
Added:
(* The later import gained 10 kg over the earlier one, shown as a
612
Added:
green weight delta. *)
606
613
Alcotest.(check bool)
607
Removed:
"the load gain since last performed is shown" true
608
Removed:
(Option.is_some (index_from ~needle:"+10 kg" page 0)) );
614
Added:
"the load gain is shown as a delta" true
615
Added:
(Option.is_some (index_from ~needle:"(+10)" page 0));
616
Added:
Alcotest.(check bool)
617
Added:
"the gain is coloured as an increase" true
618
Added:
(Option.is_some (index_from ~needle:"delta-up" page 0)) );
609
619
( "the progress page charts an exercise with enough history",
610
620
`Quick,
611
621
fun () ->