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

Commit
71390357a6199f687b0dd26ac247fdc58afbde2e
Author
Marius Peter <dev@marius-peter.com>
Author date
Committer
Marius Peter <dev@marius-peter.com>
Committer date
Changed files
lib/presenter/pages_workout.ml
index 3d2cbd0d..ad41a703 100644..100644
@@ -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
index d48d6ebb..662544d2 100644..100644
@@ -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
index 12c9f9ab..cf3de8f5 100644..100644
@@ -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
index f8282483..c0e48cc2 100644..100644
@@ -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
index 94042c2c..708b9efd 100644..100644
@@ -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 ]