[OCaml] High Intensity Training Online
fix Split prior-effort line per load/reps column
Split the combined 'Last: 12 kg x 22 reps' line under the exercise name into two separate lines, each sitting under its own load or reps input. The exercise name now has its own grid column, with load and reps as the other two columns, instead of a two-column inputs row stacked under the name. Dropped the repeated 'Last' word from each line, since the muted style and position under each input already mark the value as the prior effort to beat. sim: not tracked
Changed files
lib/presenter/pages_workout.ml
@@ -20,48 +20,48 @@
20
20
option "static" "then a static hold";
21
21
]
22
22
23
Removed:
(* One movement as a grid group: exercise name, load input, reps input. The
24
Removed:
group is a [div] that is [display:contents], so its three cells fall on the
25
Removed:
enclosing grid under the shared Load and Reps headings. A pre-exhaust pair is
26
Removed:
two such groups. *)
23
Added:
(* One movement: the exercise name sits in its own column, beside a load input
24
Added:
and a reps input. Each input carries its own record to beat directly
25
Added:
beneath it, in the same cell, so the prior load sits under the load input
26
Added:
and the prior reps sit under the reps input. A pre-exhaust pair is two such
27
Added:
movements, stacked. *)
27
28
let movement_group ~name ~load_id ~load_field ~reps_id ~reps_field ?load_value
28
Removed:
?reps_value ?previous ~reps_label () =
29
Removed:
let number_field ~input_id ~field ~value ~step_ ~label =
29
Added:
?reps_value ?previous_load ?previous_reps ~reps_label () =
30
Added:
let number_field ~input_id ~field ~value ~step_ ~label ~previous =
30
31
tag "div"
31
32
[ class_ "field field-number" ]
32
Removed:
[
33
Removed:
tag "label"
34
Removed:
[ Dream_html.string_attr "for" "%s" input_id; class_ "sr-only" ]
35
Removed:
[ txt "%s" label ];
36
Removed:
void "input"
37
Removed:
([
38
Removed:
type_ "number";
39
Removed:
Dream_html.string_attr "name" "%s" field;
40
Removed:
Dream_html.string_attr "id" "%s" input_id;
41
Removed:
step "%s" step_;
42
Removed:
required;
43
Removed:
]
44
Removed:
@
45
Removed:
match value with
46
Removed:
| None -> []
47
Removed:
| Some v -> [ Dream_html.string_attr "value" "%s" v ]);
48
Removed:
]
33
Added:
([
34
Added:
tag "label"
35
Added:
[ Dream_html.string_attr "for" "%s" input_id; class_ "sr-only" ]
36
Added:
[ txt "%s" label ];
37
Added:
void "input"
38
Added:
([
39
Added:
type_ "number";
40
Added:
Dream_html.string_attr "name" "%s" field;
41
Added:
Dream_html.string_attr "id" "%s" input_id;
42
Added:
step "%s" step_;
43
Added:
required;
44
Added:
]
45
Added:
@
46
Added:
match value with
47
Added:
| None -> []
48
Added:
| Some v -> [ Dream_html.string_attr "value" "%s" v ]);
49
Added:
]
50
Added:
@
51
Added:
match previous with
52
Added:
| Some p -> [ tag "span" [ class_ "exercise-previous" ] [ txt "%s" p ] ]
53
Added:
| None -> [])
49
54
in
50
55
tag "div"
51
56
[ class_ "movement" ]
52
57
[
53
58
tag "div"
54
59
[ class_ "exercise-name" ]
55
Removed:
(tag "span" [ class_ "exercise-name-label" ] [ txt "%s" name ]
56
Removed:
::
57
Removed:
(match previous with
58
Removed:
| Some p ->
59
Removed:
[ tag "span" [ class_ "exercise-previous" ] [ txt "Last: %s" p ] ]
60
Removed:
| None -> []));
60
Added:
[ tag "span" [ class_ "exercise-name-label" ] [ txt "%s" name ] ];
61
61
number_field ~input_id:load_id ~field:load_field ~value:load_value
62
Removed:
~step_:"0.5" ~label:"Load (kg)";
62
Added:
~step_:"0.5" ~label:"Load (kg)" ~previous:previous_load;
63
63
number_field ~input_id:reps_id ~field:reps_field ~value:reps_value
64
Removed:
~step_:"1" ~label:reps_label;
64
Added:
~step_:"1" ~label:reps_label ~previous:previous_reps;
65
65
]
66
66
67
67
(* [movements] carries the record/correction field spec the controller built
@@ -78,7 +78,8 @@
78
78
movement_group ~name:m.name ~load_id:(field_id m.load_field)
79
79
~load_field:m.load_field ~reps_id:(field_id m.reps_field)
80
80
~reps_field:m.reps_field ?load_value:m.load_value ?reps_value:m.reps_value
81
Removed:
?previous:m.previous ~reps_label:m.reps_label ()
81
Added:
?previous_load:m.previous_load ?previous_reps:m.previous_reps
82
Added:
~reps_label:m.reps_label ()
82
83
in
83
84
let movement_groups = List.map movement_of movements in
84
85
tag "form"
@@ -91,11 +92,11 @@
91
92
Dream_html.csrf_tag request;
92
93
tag "fieldset"
93
94
[ class_ "logging-fieldset" ]
94
Removed:
((* The set as a grid of movement groups under a shared pair of column
95
Removed:
headings. Each movement group lays its name, load, and reps onto the
96
Removed:
grid. The per-input labels carry the accessible names, so the two
97
Removed:
visible headings are decorative. The Ending block is separate,
98
Removed:
below the grid. *)
95
Added:
((* The set as a list of movement groups under a shared row of column
96
Added:
headings. Each movement is a three-column row: the exercise name,
97
Added:
then the load input, then the reps input. The per-input labels
98
Added:
carry the accessible names, so the two visible headings are
99
Added:
decorative. The Ending block is separate, below the list. *)
99
100
[
100
101
tag "div"
101
102
[ class_ "logging-grid" ]
lib/presenter/view_model.ml
@@ -97,11 +97,15 @@
97
97
load_value : string option;
98
98
(** Pre-fill when correcting a recorded slot. *)
99
99
reps_value : string option;
100
Removed:
previous : string option;
101
Removed:
(** The most recent prior effort for this exercise, pre-formatted as the
102
Removed:
record to beat (for example ["60 kg × 12 reps"]). [None] when the
103
Removed:
exercise has no earlier observation. It is an observed fact, not a
104
Removed:
target — the set still ends at failure. *)
100
Added:
previous_load : string option;
101
Added:
(** The most recent prior load for this exercise, pre-formatted as the
102
Added:
record to beat (for example ["12 kg"]). [None] when the exercise has
103
Added:
no earlier observation. It is an observed fact, not a target — the
104
Added:
set still ends at failure. *)
105
Added:
previous_reps : string option;
106
Added:
(** The most recent prior reps for this exercise, pre-formatted (for
107
Added:
example ["22 reps"]). [None] exactly when [previous_load] is [None]
108
Added:
— the pair comes from the same observation. *)
105
109
}
106
110
(** One input row of a slot's record/correction form. The controller emits one
107
111
movement for a single-exercise slot and two for a pre-exhaust pair, so the
lib/web/assets/hito.css
@@ -941,55 +941,57 @@
941
941
.chart-series.reps { stroke: var(--brass); stroke-dasharray: 5 3; }
942
942
943
943
/* --- Workout logging fieldset ---
944
Removed:
The set is a grid of movement groups, so the three columns hold the golden
945
Removed:
ratio. The exercise name is the smaller part at 1fr. Load and reps take the
946
Removed:
wider portion, split evenly at 0.809fr each. Each movement group and the
947
Removed:
heading row use display:contents, so their cells land on the grid. The set
948
Removed:
ending, which is the extension select and an optional amount, is a separate
949
Removed:
row below the grid on the same three columns. */
944
Added:
Each movement is a three-column row: the exercise name, then the load
945
Added:
input, then the reps input. Movements stack vertically with a gap between
946
Added:
them. There is no shared grid across movements — each movement sizes its
947
Added:
own three columns, matching the heading row's columns by the same
948
Added:
template, not by implicit position. */
950
949
.exercise-name-label {
951
950
display: block;
952
951
color: var(--ink);
953
952
font: 700 var(--font-size-lead)/1.2 var(--sans);
954
953
letter-spacing: -0.01em;
955
954
}
956
Removed:
/* The record to beat: the exercise's most recent prior effort, an observed fact
957
Removed:
under its name, muted so it does not compete with the inputs. */
955
Added:
/* The record to beat for one input: the exercise's most recent prior load or
956
Added:
reps, sitting in the same cell under its input, muted so it does not
957
Added:
compete with the field above it. */
958
958
.exercise-previous {
959
959
display: block;
960
Removed:
margin: 0.15rem 0 0;
960
Added:
margin: 0.3rem 0 0;
961
961
color: var(--muted-ink);
962
962
font: 500 var(--font-size-small)/1.2 var(--mono);
963
963
font-variant-numeric: tabular-nums;
964
964
}
965
965
966
966
.logging-grid {
967
Added:
margin-top: 1rem;
968
Added:
}
969
Added:
/* The Load/Reps heading sits once above the first movement's inputs. Its own
970
Added:
three columns match every movement row below it: a wide name column, then
971
Added:
two equal, auto-sized input columns. The name cell is empty and decorative. */
972
Added:
.logging-grid-head {
967
973
display: grid;
968
Removed:
/* The golden ratio separates the name column from the two number columns.
969
Removed:
The name is the smaller part at 1. Load and reps together are the larger
970
Removed:
part, about 1.618, split evenly at 0.809fr each. */
971
974
grid-template-columns: minmax(0, 1fr) minmax(0, 0.809fr) minmax(0, 0.809fr);
972
Removed:
align-items: center;
973
Removed:
gap: 0.35rem 0.6rem;
974
Removed:
margin-top: 1rem;
975
Added:
gap: 0.6rem;
976
Added:
padding-bottom: 0.35rem;
975
977
}
976
Removed:
/* The heading row and each movement group flatten onto the grid so their cells
977
Removed:
land in the three columns. */
978
Removed:
.logging-grid-head,
979
Removed:
.movement {
980
Removed:
display: contents;
981
Removed:
}
982
Removed:
/* The two visible column headings sit over the number columns. The name
983
Removed:
column has no heading, because the names describe themselves. */
984
978
.logging-grid-head-load,
985
979
.logging-grid-head-reps {
986
Removed:
padding-bottom: 0.35rem;
987
980
color: var(--muted-ink);
988
981
font: 700 var(--font-size-small)/1 var(--sans);
989
Removed:
align-self: end;
990
982
}
991
Removed:
.logging-grid .field-number { padding: 0; }
992
Removed:
.logging-grid input { width: 100%; }
983
Added:
.movement {
984
Added:
display: grid;
985
Added:
grid-template-columns: minmax(0, 1fr) minmax(0, 0.809fr) minmax(0, 0.809fr);
986
Added:
align-items: start;
987
Added:
gap: 0.6rem;
988
Added:
margin-top: 1rem;
989
Added:
}
990
Added:
.movement:first-child {
991
Added:
margin-top: 0;
992
Added:
}
993
Added:
.movement .field-number { padding: 0; margin: 0; }
994
Added:
.movement input { width: 100%; }
993
995
994
996
/* The set ending sits below the movements and shares the logging grid's
995
997
geometry. Each control takes one column: the select under the names, the
lib/web/handlers_kit.ml
@@ -356,11 +356,11 @@
356
356
match List.rev (Evidence.Log.observations log exercise) with
357
357
| latest :: _ ->
358
358
let e = Evidence.Observation.effort latest in
359
Removed:
Some
360
Removed:
(Printf.sprintf "%s kg × %d reps"
361
Removed:
(load_string (Evidence.Stimulus.Effort.load e))
362
Removed:
(Evidence.Stimulus.Effort.reps e))
363
Removed:
| [] -> None
359
Added:
( Some
360
Added:
(Printf.sprintf "%s kg"
361
Added:
(load_string (Evidence.Stimulus.Effort.load e))),
362
Added:
Some (Printf.sprintf "%d reps" (Evidence.Stimulus.Effort.reps e)) )
363
Added:
| [] -> (None, None)
364
364
in
365
365
let label p =
366
366
match Prescription.Stimulus.delivery p with
@@ -386,6 +386,7 @@
386
386
let movements : View_model.Workout.movement list =
387
387
match Prescription.Stimulus.delivery p with
388
388
| Prescription.Stimulus.Single exercise ->
389
Added:
let previous_load, previous_reps = previous_of exercise in
389
390
[
390
391
{
391
392
name = Exercise.name exercise;
@@ -394,10 +395,13 @@
394
395
reps_label = "Reps to failure";
395
396
load_value = load_of 0;
396
397
reps_value = reps_of 0;
397
Removed:
previous = previous_of exercise;
398
Added:
previous_load;
399
Added:
previous_reps;
398
400
};
399
401
]
400
402
| Prescription.Stimulus.Pre_exhaust { isolation; compound } ->
403
Added:
let iso_previous_load, iso_previous_reps = previous_of isolation in
404
Added:
let comp_previous_load, comp_previous_reps = previous_of compound in
401
405
[
402
406
{
403
407
name = Exercise.name isolation;
@@ -406,7 +410,8 @@
406
410
reps_label = "Reps";
407
411
load_value = load_of 0;
408
412
reps_value = reps_of 0;
409
Removed:
previous = previous_of isolation;
413
Added:
previous_load = iso_previous_load;
414
Added:
previous_reps = iso_previous_reps;
410
415
};
411
416
{
412
417
name = Exercise.name compound;
@@ -415,7 +420,8 @@
415
420
reps_label = "Reps";
416
421
load_value = load_of 1;
417
422
reps_value = reps_of 1;
418
Removed:
previous = previous_of compound;
423
Added:
previous_load = comp_previous_load;
424
Added:
previous_reps = comp_previous_reps;
419
425
};
420
426
]
421
427
in
test/web/workout.ml
@@ -799,8 +799,10 @@
799
799
(Option.is_some
800
800
(index_from ~needle:"exercise-previous" day_one_again 0));
801
801
Alcotest.(check bool)
802
Removed:
"the prior load and reps are stated" true
803
Removed:
(Option.is_some
804
Removed:
(index_from ~needle:"Last: 12 kg × 8 reps" day_one_again 0)) );
802
Added:
"the prior load is stated under the load input" true
803
Added:
(Option.is_some (index_from ~needle:"12 kg" day_one_again 0));
804
Added:
Alcotest.(check bool)
805
Added:
"the prior reps are stated under the reps input" true
806
Added:
(Option.is_some (index_from ~needle:"8 reps" day_one_again 0)) );
805
807
] );
806
808
]