feat Refine workout form layout

Align correction actions with the logging grid and use main as the replaceable app content node. Remove redundant mobile surface padding.

Commit
f0743128652c40b0f4c14cf309bf6503ad093927
Author
Marius Peter <dev@marius-peter.com>
Author date
Committer
Marius Peter <dev@marius-peter.com>
Committer date
Changed files
ARCHITECTURE.org
index 1f5891a5..4552b587 100644..100644
@@ -535,7 +535,7 @@
535 535 | Marker | Meaning |
536 536 |----------------------------+-----------------------------------------------|
537 537 | =data-hito-app-shell= | the replaceable shell, carrying the page title |
538 Removed: | =data-hito-app-content= | the page surface inside it |
538 Added: | =data-hito-app-content= | the replaceable app content node inside the shell |
539 539 | =data-hito-app-link= | a link eligible for in-place navigation |
540 540 | =data-hito-app-form= | a form eligible for in-place submission |
541 541 | =data-hito-workout-*= | the same, for current-workout slots, forms, and the timer |
lib/presenter/page_shell.ml
index 67083ce1..b6f21665 100644..100644
@@ -245,17 +245,12 @@
245 245 ]
246 246 @ primary_nav @ profile_area);
247 247 tag "main"
248 Removed: [ id "main-content"; tabindex "-1" ]
249 Removed: [
250 Removed: tag "div"
251 Removed: ([ class_ "page-surface" ]
252 Removed: @
253 Removed: if spa_client then
254 Removed: [ Dream_html.attr "data-hito-app-content" ]
255 Removed: else [])
256 Removed: content;
257 Removed: footer ();
258 Removed: ];
248 Added: ([ id "main-content"; tabindex "-1" ]
249 Added: @
250 Added: if spa_client then [ Dream_html.attr "data-hito-app-content" ]
251 Added: else [])
252 Added: content;
253 Added: footer ();
259 254 ]
260 255 @ bottom_nav);
261 256 ]
lib/presenter/pages_workout.ml
index ad41a703..790a1fbd 100644..100644
@@ -21,48 +21,60 @@
21 21 ]
22 22
23 23 (* One movement: the exercise name sits in its own column, beside a load input
24 Removed: and a reps input. Each input carries its own record to beat directly
25 Removed: beneath it, in the same cell, so the prior load sits under the load input
26 Removed: and the prior reps sit under the reps input. A pre-exhaust pair is two such
27 Removed: movements, stacked. *)
24 Added: and a reps input. The function returns its three fields directly, so the
25 Added: logging fieldset can place every movement in one shared grid. Each input
26 Added: group shows an optional prior value, the current input, and its unit. *)
28 27 let movement_group ~name ~load_id ~load_field ~reps_id ~reps_field ?load_value
29 28 ?reps_value ?previous_load ?previous_reps ~reps_label () =
30 Removed: let number_field ~input_id ~field ~value ~step_ ~label ~previous =
31 Removed: tag "div"
32 Removed: [ class_ "field field-number" ]
33 Removed: ([
34 Removed: tag "label"
35 Removed: [ Dream_html.string_attr "for" "%s" input_id; class_ "sr-only" ]
36 Removed: [ txt "%s" label ];
37 Removed: void "input"
38 Removed: ([
39 Removed: type_ "number";
40 Removed: Dream_html.string_attr "name" "%s" field;
41 Removed: Dream_html.string_attr "id" "%s" input_id;
42 Removed: step "%s" step_;
43 Removed: required;
44 Removed: ]
45 Removed: @
46 Removed: match value with
47 Removed: | None -> []
48 Removed: | Some v -> [ Dream_html.string_attr "value" "%s" v ]);
49 Removed: ]
50 Removed: @
29 Added: let number_field ~input_id ~field ~value ~step_ ~label ~previous ~unit =
30 Added: let input =
31 Added: void "input"
32 Added: ([
33 Added: type_ "number";
34 Added: Dream_html.string_attr "name" "%s" field;
35 Added: Dream_html.string_attr "id" "%s" input_id;
36 Added: step "%s" step_;
37 Added: required;
38 Added: ]
39 Added: @
40 Added: match value with
41 Added: | None -> []
42 Added: | Some v -> [ Dream_html.string_attr "value" "%s" v ])
43 Added: in
44 Added: let group_class =
51 45 match previous with
46 Added: | None -> "input-group"
47 Added: | Some _ -> "input-group input-group-with-previous"
48 Added: in
49 Added: let previous_control =
50 Added: match previous with
51 Added: | None -> []
52 52 | Some p -> [ tag "span" [ class_ "exercise-previous" ] [ txt "%s" p ] ]
53 Removed: | None -> [])
54 Removed: in
55 Removed: tag "div"
56 Removed: [ class_ "movement" ]
57 Removed: [
53 Added: in
54 Added: let control =
58 55 tag "div"
59 Removed: [ class_ "exercise-name" ]
60 Removed: [ tag "span" [ class_ "exercise-name-label" ] [ txt "%s" name ] ];
61 Removed: number_field ~input_id:load_id ~field:load_field ~value:load_value
62 Removed: ~step_:"0.5" ~label:"Load (kg)" ~previous:previous_load;
63 Removed: number_field ~input_id:reps_id ~field:reps_field ~value:reps_value
64 Removed: ~step_:"1" ~label:reps_label ~previous:previous_reps;
65 Removed: ]
56 Added: [ Dream_html.string_attr "class" "%s" group_class ]
57 Added: (previous_control
58 Added: @ [ input; tag "span" [ class_ "input-unit" ] [ txt "%s" unit ] ])
59 Added: in
60 Added: tag "div"
61 Added: [ class_ "field field-number" ]
62 Added: [
63 Added: tag "label"
64 Added: [ Dream_html.string_attr "for" "%s" input_id; class_ "sr-only" ]
65 Added: [ txt "%s" label ];
66 Added: control;
67 Added: ]
68 Added: in
69 Added: [
70 Added: tag "div"
71 Added: [ class_ "exercise-name" ]
72 Added: [ tag "span" [ class_ "exercise-name-label" ] [ txt "%s" name ] ];
73 Added: number_field ~input_id:load_id ~field:load_field ~value:load_value
74 Added: ~step_:"0.5" ~label:"Load (kg)" ~previous:previous_load ~unit:"kg";
75 Added: number_field ~input_id:reps_id ~field:reps_field ~value:reps_value
76 Added: ~step_:"1" ~label:reps_label ~previous:previous_reps ~unit:"reps";
77 Added: ]
66 78
67 79 (* [movements] carries the record/correction field spec the controller built
68 80 from the slot's prescription: one row for a single exercise, two for a
@@ -81,7 +93,7 @@
81 93 ?previous_load:m.previous_load ?previous_reps:m.previous_reps
82 94 ~reps_label:m.reps_label ()
83 95 in
84 Removed: let movement_groups = List.map movement_of movements in
96 Added: let movement_groups = List.concat_map movement_of movements in
85 97 tag "form"
86 98 ([ action_path; post_form ]
87 99 @ (if enhanced then [ Dream_html.attr "data-hito-workout-form" ]
@@ -92,62 +104,40 @@
92 104 Dream_html.csrf_tag request;
93 105 tag "fieldset"
94 106 [ class_ "logging-fieldset" ]
95 Removed: ((* The set as a list of movement groups under a shared row of column
96 Removed: headings. Each movement is a three-column row: the exercise name,
97 Removed: then the load input, then the reps input. The per-input labels
98 Removed: carry the accessible names, so the two visible headings are
99 Removed: decorative. The Ending block is separate, below the list. *)
100 Removed: [
101 Removed: tag "div"
102 Removed: [ class_ "logging-grid" ]
103 Removed: (tag "div"
104 Removed: [
105 Removed: class_ "logging-grid-head";
106 Removed: Dream_html.string_attr "aria-hidden" "true";
107 Removed: ]
108 Removed: [
109 Removed: tag "span" [ class_ "logging-grid-head-name" ] [];
110 Removed: tag "span"
111 Removed: [ class_ "logging-grid-head-load" ]
112 Removed: [ txt "Load (kg)" ];
113 Removed: tag "span" [ class_ "logging-grid-head-reps" ] [ txt "Reps" ];
114 Removed: ]
115 Removed: :: movement_groups);
116 Removed: ]
107 Added: (movement_groups
117 108 @ [
118 109 tag "div"
119 Removed: [ class_ "ending-block" ]
110 Added: [ class_ "field field-ending" ]
120 111 [
121 Removed: tag "div"
122 Removed: [ class_ "field field-ending" ]
112 Added: tag "label"
123 113 [
124 Removed: tag "label"
125 Removed: [
126 Removed: Dream_html.string_attr "for" "%s" (field_id "extension");
127 Removed: class_ "sr-only";
128 Removed: ]
129 Removed: [ txt "Ending" ];
130 Removed: extension_select ~selected ~input_id:(field_id "extension")
131 Removed: ();
114 Added: Dream_html.string_attr "for" "%s" (field_id "extension");
115 Added: class_ "sr-only";
116 Added: ]
117 Added: [ txt "Ending" ];
118 Added: extension_select ~selected ~input_id:(field_id "extension") ();
119 Added: ];
120 Added: tag "div"
121 Added: [ class_ "field field-extension-amount" ]
122 Added: [
123 Added: tag "label"
124 Added: [
125 Added: Dream_html.string_attr "for" "%s"
126 Added: (field_id "extension_amount");
127 Added: class_ "sr-only";
128 Added: ]
129 Added: [
130 Added: tag "span"
131 Added: [ class_ "amount-label-reps" ]
132 Added: [ txt "Extension reps" ];
133 Added: tag "span" [ class_ "amount-label-sep" ] [ txt " / " ];
134 Added: tag "span"
135 Added: [ class_ "amount-label-seconds" ]
136 Added: [ txt "Hold seconds" ];
132 137 ];
133 138 tag "div"
134 Removed: [ class_ "field field-extension-amount" ]
139 Added: [ class_ "input-group" ]
135 140 [
136 Removed: tag "label"
137 Removed: [
138 Removed: Dream_html.string_attr "for" "%s"
139 Removed: (field_id "extension_amount");
140 Removed: class_ "sr-only";
141 Removed: ]
142 Removed: [
143 Removed: tag "span"
144 Removed: [ class_ "amount-label-reps" ]
145 Removed: [ txt "Extension reps" ];
146 Removed: tag "span" [ class_ "amount-label-sep" ] [ txt " / " ];
147 Removed: tag "span"
148 Removed: [ class_ "amount-label-seconds" ]
149 Removed: [ txt "Hold seconds" ];
150 Removed: ];
151 141 void "input"
152 142 [
153 143 type_ "number";
@@ -160,7 +150,7 @@
160 150 selected_extension_amount;
161 151 ];
162 152 tag "span"
163 Removed: [ class_ "field-unit" ]
153 Added: [ class_ "input-unit" ]
164 154 [
165 155 tag "span" [ class_ "amount-label-reps" ] [ txt "reps" ];
166 156 tag "span" [ class_ "amount-label-sep" ] [ txt " / " ];
@@ -174,22 +164,20 @@
174 164 @
175 165 if correction then
176 166 [
177 Removed: tag "div"
167 Added: void "input"
178 168 [
179 Removed: class_ "button-group correction-actions";
180 Removed: Dream_html.attr "data-hito-correction-actions";
181 Removed: ]
182 Removed: [
183 Removed: void "input"
184 Removed: [ type_ "submit"; Dream_html.string_attr "value" "%s" submit ];
185 Removed: tag "button"
186 Removed: [
187 Removed: type_ "reset";
188 Removed: class_ "secondary";
189 Removed: Dream_html.attr "data-hito-correction-reset";
190 Removed: ]
191 Removed: [ txt "Reset" ];
169 Added: type_ "submit";
170 Added: Dream_html.string_attr "value" "%s" submit;
171 Added: Dream_html.attr "data-hito-correction-action";
192 172 ];
173 Added: tag "button"
174 Added: [
175 Added: type_ "reset";
176 Added: class_ "secondary";
177 Added: Dream_html.attr "data-hito-correction-action";
178 Added: Dream_html.attr "data-hito-correction-reset";
179 Added: ]
180 Added: [ txt "Reset" ];
193 181 ]
194 182 else
195 183 [
lib/presenter/view_model.ml
index 6c2239ae..5151f904 100644..100644
@@ -99,13 +99,14 @@
99 99 reps_value : string option;
100 100 previous_load : string option;
101 101 (** The most recent prior load for this exercise, pre-formatted as the
102 Removed: record to beat (for example ["12 kg"]). [None] when the exercise has
103 Removed: no earlier observation. It is an observed fact, not a target — the
104 Removed: set still ends at failure. *)
102 Added: numeric record to beat (for example ["12"]). [None] when the
103 Added: exercise has no earlier observation. It is an observed fact, not a
104 Added: target — the set still ends at failure. *)
105 105 previous_reps : string option;
106 Removed: (** The most recent prior reps for this exercise, pre-formatted (for
107 Removed: example ["22 reps"]). [None] exactly when [previous_load] is [None]
108 Removed: — the pair comes from the same observation. *)
106 Added: (** The most recent prior reps for this exercise, pre-formatted as the
107 Added: numeric record to beat (for example ["22"]). [None] exactly when
108 Added: [previous_load] is [None] — the pair comes from the same
109 Added: observation. *)
109 110 }
110 111 (** One input row of a slot's record/correction form. The controller emits one
111 112 movement for a single-exercise slot and two for a pre-exhaust pair, so the
lib/web/assets/hito.css
index ba81fd41..482b851f 100644..100644
@@ -3,7 +3,7 @@
3 3 Google Fonts @import that left Courier Prime absent on slow mobile networks.
4 4 Work Sans ships as one variable WOFF2 covering the 400-800 weight axis.
5 5 Courier Prime is static, so it needs one face per weight. Latin subset only;
6 Removed: no italic face is used anywhere in the app. */
6 Added: prior values use a synthetic italic face. */
7 7 @font-face {
8 8 font-family: "Work Sans";
9 9 font-style: normal;
@@ -319,20 +319,15 @@
319 319 margin: 0;
320 320 }
321 321
322 Removed: /* Masthead padding creates the separation from the page card. Keep only the
323 Removed: bottom padding here so the footer still clears the content. */
324 Removed: main { padding: 0 0 1.5rem; }
325 Removed:
326 Removed: .bottom-nav { display: none; }
327 Removed:
328 Removed: /* Flat and native: the content sits directly on the page, no card chrome. The
329 Removed: blood brand marker now lives on the masthead, so the content starts clean.
330 Removed: The desktop breakpoint only constrains the reading width. */
331 Removed: .page-surface {
322 Added: /* The content starts below the masthead. The main element is also the
323 Added: replaceable SPA content node, so the page needs no extra surface wrapper. */
324 Added: main {
332 325 min-height: 26rem;
333 Removed: padding: 1.25rem 0 0;
326 Added: padding: 1.25rem 0 1.5rem;
334 327 }
335 328
329 Added: .bottom-nav { display: none; }
330 Added:
336 331 /* Page transitions. The SPA client swaps the content node when the user moves
337 332 between Home, Current Workout, and Logbook. A freshly inserted node runs
338 333 this fade-and-rise, so each transition animates without script. The global
@@ -346,20 +341,11 @@
346 341 animation: page-enter 220ms ease both;
347 342 }
348 343
349 Removed: /* Keep the desktop masthead and navigation wide, and give reading content a
350 Removed: shorter line length. The raised card returns at this width. Mobile stays
351 Removed: flat and native. */
352 Removed: /* On desktop the shell and content settle at a compact, readable 50rem. They
353 Removed: do not stretch with the viewport. The shell frames the masthead, content,
354 Removed: and footer. The content fills the shell. */
344 Added: /* On desktop the shell settles at a compact, readable width. */
355 345 @media (min-width: 42.0625rem) {
356 346 .app-shell {
357 347 width: min(100%, 50rem);
358 348 }
359 Removed: .page-surface {
360 Removed: width: 100%;
361 Removed: margin-inline: auto;
362 Removed: }
363 349 }
364 350
365 351 /* One coherent heading scale across the app. h1 is the page title, h2 a
@@ -585,11 +571,10 @@
585 571
586 572 /* Blood outline: a blood border and blood text on a transparent field
587 573 that fills blood on hover. Shared by the anchor outline variant and the
588 Removed: correction and finish action groups, which name both group classes so they
589 Removed: beat the generic solid-fill and brass-outline rules of equal specificity. */
574 Added: correction and finish submit controls. */
590 575 a.button.outline,
591 576 input[type="submit"].outline,
592 Removed: .button-group.correction-actions input[type="submit"],
577 Added: [data-hito-correction-action][type="submit"],
593 578 .button-group.finish-actions input[type="submit"],
594 579 .button-group.finish-actions input[type="submit"].secondary {
595 580 color: var(--blood);
@@ -598,39 +583,24 @@
598 583 }
599 584 a.button.outline:hover,
600 585 input[type="submit"].outline:hover,
601 Removed: .button-group.correction-actions input[type="submit"]:hover,
586 Added: [data-hito-correction-action][type="submit"]:hover,
602 587 .button-group.finish-actions input[type="submit"]:hover,
603 588 .button-group.finish-actions input[type="submit"].secondary:hover {
604 589 color: var(--on-primary);
605 590 background: var(--blood);
606 591 }
607 592
608 Removed: /* The correction actions, Save correction and Reset, appear only after a
609 Removed: recorded value changes. With JavaScript, the pair stays hidden until the
610 Removed: client marks the form dirty. The reset button then restores the recorded
611 Removed: values. That clears the dirty state and hides the pair again. Without
612 Removed: JavaScript the pair stays visible, so a correction is always possible. The
613 Removed: buttons share the row at equal width. */
614 Removed: .correction-actions {
615 Removed: display: grid;
616 Removed: grid-template-columns: 1fr 1fr;
617 Removed: gap: 0.6rem;
618 Removed: }
619 Removed: [data-hito-js="on"] .correction-actions[data-hito-correction-actions] {
593 Added: /* Correction controls are direct items in the logging fieldset. JavaScript
594 Added: hides them until the form becomes dirty. Without JavaScript, both controls
595 Added: remain available. */
596 Added: [data-hito-js="on"] form[data-hito-correction-form]
597 Added: [data-hito-correction-action] {
620 598 display: none;
621 599 }
622 Removed: [data-hito-js="on"]
623 Removed: form[data-hito-dirty]
624 Removed: .correction-actions[data-hito-correction-actions] {
625 Removed: display: grid;
600 Added: [data-hito-js="on"] form[data-hito-correction-form][data-hito-dirty]
601 Added: [data-hito-correction-action] {
602 Added: display: inline-flex;
626 603 }
627 Removed: /* Both correction controls sit in one grid row, so neither carries its own
628 Removed: top margin. The group's top margin is the single source of vertical
629 Removed: spacing. */
630 Removed: .button-group.correction-actions input[type="submit"],
631 Removed: .button-group.correction-actions button[type="reset"].secondary {
632 Removed: margin-top: 0;
633 Removed: }
634 604
635 605 /* Keeps the finish and cancel controls together as equal-width actions. */
636 606 .button-group {
@@ -944,9 +914,13 @@
944 914 /* --- Workout logging fieldset ---
945 915 Each movement is a three-column row: the exercise name, then the load
946 916 input, then the reps input. Movements stack vertically with a gap between
947 Removed: them. There is no shared grid across movements — each movement sizes its
948 Removed: own three columns, matching the heading row's columns by the same
949 Removed: template, not by implicit position. */
917 Added: them. Each input group carries its own unit, so no separate heading row is
918 Added: needed. */
919 Added: .exercise-name {
920 Added: display: flex;
921 Added: align-items: center;
922 Added: min-height: 3rem;
923 Added: }
950 924 .exercise-name-label {
951 925 display: block;
952 926 color: var(--ink);
@@ -954,68 +928,86 @@
954 928 letter-spacing: -0.01em;
955 929 }
956 930 /* The record to beat for one input: the exercise's most recent prior load or
957 Removed: reps, sitting in the same cell under its input, muted so it does not
958 Removed: compete with the field above it. */
931 Added: reps, integrated into the input as a Bootstrap-style leading group. It is
932 Added: italic and monospaced so it stays distinct from the value being entered. */
959 933 .exercise-previous {
960 Removed: display: block;
961 Removed: margin: 0.3rem 0 0;
934 Added: display: flex;
935 Added: align-items: center;
936 Added: flex: 0 0 auto;
937 Added: min-height: 3rem;
938 Added: margin: 0;
939 Added: padding: 0.65rem 0.75rem;
940 Added: border: 1px solid var(--rule-strong);
941 Added: border-right: 0;
942 Added: border-radius: var(--radius) 0 0 var(--radius);
962 943 color: var(--muted-ink);
963 Removed: font: 500 var(--font-size-small)/1.2 var(--mono);
944 Added: background: var(--hover-surface);
945 Added: font: 500 1rem/1.3 var(--mono);
946 Added: font-style: italic;
964 947 font-variant-numeric: tabular-nums;
948 Added: white-space: nowrap;
965 949 }
966 Removed:
967 Removed: .logging-grid {
968 Removed: margin-top: 1rem;
950 Added: .input-group {
951 Added: display: flex;
952 Added: align-items: stretch;
953 Added: width: 100%;
969 954 }
970 Removed: /* The Load/Reps heading sits once above the first movement's inputs. Its own
971 Removed: three columns match every movement row below it: a wide name column, then
972 Removed: two equal, auto-sized input columns. The name cell is empty and decorative. */
973 Removed: .logging-grid-head {
974 Removed: display: grid;
975 Removed: grid-template-columns: minmax(0, 1fr) minmax(0, 0.809fr) minmax(0, 0.809fr);
976 Removed: gap: 0.6rem;
977 Removed: padding-bottom: 0.35rem;
955 Added: .input-group input {
956 Added: flex: 1 1 auto;
957 Added: min-width: 0;
958 Added: border-radius: 0;
978 959 }
979 Removed: .logging-grid-head-load,
980 Removed: .logging-grid-head-reps {
960 Added: .input-group:not(.input-group-with-previous) input {
961 Added: border-radius: var(--radius) 0 0 var(--radius);
962 Added: }
963 Added: .input-unit {
964 Added: display: flex;
965 Added: align-items: center;
966 Added: flex: 0 0 auto;
967 Added: min-height: 3rem;
968 Added: padding: 0.65rem 0.75rem;
969 Added: border: 1px solid var(--rule-strong);
970 Added: border-left: 0;
971 Added: border-radius: 0 var(--radius) var(--radius) 0;
981 972 color: var(--muted-ink);
982 Removed: font: 700 var(--font-size-small)/1 var(--sans);
973 Added: background: var(--hover-surface);
974 Added: font: 500 1rem/1.3 var(--mono);
975 Added: font-variant-numeric: tabular-nums;
976 Added: white-space: nowrap;
983 977 }
984 Removed: .movement {
978 Added:
979 Added: .logging-fieldset {
985 980 display: grid;
986 Removed: grid-template-columns: minmax(0, 1fr) minmax(0, 0.809fr) minmax(0, 0.809fr);
981 Added: grid-template-columns: repeat(3, minmax(0, 1fr));
987 982 align-items: start;
988 Removed: gap: 0.6rem;
983 Added: column-gap: 0.6rem;
984 Added: row-gap: 1rem;
989 985 margin-top: 1rem;
990 986 }
991 Removed: .movement:first-child {
987 Added: .logging-fieldset > .field { margin: 0; min-width: 0; }
988 Added: .logging-fieldset > .field-ending { grid-column: 1 / 3; }
989 Added: .logging-fieldset > .field-extension-amount { grid-column: 3; }
990 Added: .logging-fieldset > [data-hito-correction-action] {
991 Added: display: inline-flex;
992 Added: align-items: center;
993 Added: justify-content: center;
994 Added: width: 100%;
992 995 margin-top: 0;
993 996 }
994 Removed: .movement .field-number { padding: 0; margin: 0; }
995 Removed: .movement input { width: 100%; }
996 Removed:
997 Removed: /* The set ending sits below the movements and shares the logging grid's
998 Removed: geometry. Each control takes one column: the select under the names, the
999 Removed: amount under Load, and its unit under Reps. Positive failure hides the
1000 Removed: amount, and the select then spans all three columns. See the :has block. */
1001 Removed: .ending-block {
1002 Removed: display: grid;
1003 Removed: grid-template-columns: minmax(0, 1fr) minmax(0, 0.809fr) minmax(0, 0.809fr);
1004 Removed: align-items: center;
1005 Removed: gap: 0.6rem;
1006 Removed: margin-top: 0.6rem;
997 Added: .logging-fieldset > input[type="submit"]:not([data-hito-correction-action]) {
998 Added: grid-column: 1 / -1;
1007 999 }
1008 Removed: .ending-block .field { margin: 0; min-width: 0; }
1009 Removed: .ending-block .field-ending { grid-column: 1; }
1010 Removed: /* The amount field flattens onto the grid, so its input and its unit each
1011 Removed: take a column. Its label is visually hidden and out of flow. */
1012 Removed: .ending-block .field-extension-amount { display: contents; }
1013 Removed: .ending-block .field-extension-amount input { grid-column: 2; }
1014 Removed: .ending-block .field-unit {
1000 Added: form[data-hito-correction-form]
1001 Added: .logging-fieldset > [data-hito-correction-action][type="submit"] {
1002 Added: grid-column: 1 / span 2;
1003 Added: align-self: stretch;
1004 Added: margin: 0 !important;
1005 Added: }
1006 Added: form[data-hito-correction-form]
1007 Added: .logging-fieldset > [data-hito-correction-action][type="reset"] {
1015 1008 grid-column: 3;
1016 Removed: color: var(--muted-ink);
1017 Removed: font: 500 var(--font-size-small)/1 var(--sans);
1018 Removed: white-space: nowrap;
1009 Added: align-self: stretch;
1010 Added: margin: 0 !important;
1019 1011 }
1020 1012
1021 1013 /* The extension amount follows the live selection with no script, because
@@ -1027,9 +1019,7 @@
1027 1019 .field-extension-amount {
1028 1020 display: none;
1029 1021 }
1030 Removed: /* With no amount, the select spans the whole grid. */
1031 1022 .logging-fieldset:has(select[name="extension"] option[value=""]:checked)
1032 Removed: .ending-block
1033 1023 .field-ending {
1034 1024 grid-column: 1 / -1;
1035 1025 }
@@ -1059,7 +1049,7 @@
1059 1049
1060 1050 /* Home shows this affordance while a workout is in progress. It is a
1061 1051 full-width pressable panel that returns the user to the current workout. A
1062 Removed: left blood accent marks the panel. It is not the page-surface card. */
1052 Added: left blood accent marks the panel. It is independent of the page content. */
1063 1053 .in-progress {
1064 1054 display: block;
1065 1055 margin: 1.25rem 0 0;
@@ -1343,7 +1333,9 @@
1343 1333 padding: 0.9rem 0 0.7rem;
1344 1334 }
1345 1335
1346 Removed: .page-surface { min-height: calc(100vh - 9rem); padding: 1.25rem; }
1336 Added: main {
1337 Added: min-height: calc(100vh - 9rem);
1338 Added: }
1347 1339 }
1348 1340
1349 1341 @media (max-width: 26rem) {
lib/web/handlers_kit.ml
index c0e48cc2..7c5d2c1b 100644..100644
@@ -349,17 +349,16 @@
349 349 in
350 350 let load_string load = Printf.sprintf "%g" load in
351 351 let reps_string reps = string_of_int reps in
352 Removed: (* The most recent prior effort for [exercise], pre-formatted as the record
353 Removed: to beat. The log holds finished history only, so the in-progress workout
354 Removed: is never compared against itself. An observed fact, not a target. *)
352 Added: (* The most recent prior effort for [exercise], as numeric strings for the
353 Added: record to beat. The presenter adds the unit suffix. The log holds
354 Added: finished history only, so the in-progress workout is never compared
355 Added: against itself. An observed fact, not a target. *)
355 356 let previous_of exercise =
356 357 match List.rev (Evidence.Log.observations log exercise) with
357 358 | latest :: _ ->
358 359 let e = Evidence.Observation.effort latest in
359 Removed: ( Some
360 Removed: (Printf.sprintf "%s kg"
361 Removed: (load_string (Evidence.Stimulus.Effort.load e))),
362 Removed: Some (Printf.sprintf "%d reps" (Evidence.Stimulus.Effort.reps e)) )
360 Added: ( Some (load_string (Evidence.Stimulus.Effort.load e)),
361 Added: Some (reps_string (Evidence.Stimulus.Effort.reps e)) )
363 362 | [] -> (None, None)
364 363 in
365 364 let label p =
test/web/workout.ml
index e3bdd0a3..2ce2bd90 100644..100644
@@ -684,10 +684,13 @@
684 684 (Option.is_some
685 685 (index_from ~needle:"data-hito-correction-form" page 0));
686 686 Alcotest.(check bool)
687 Removed: "the correction actions pair Save and Reset" true
687 Added: "the correction actions are direct fieldset items" true
688 688 (Option.is_some
689 Removed: (index_from ~needle:"data-hito-correction-actions" page 0));
689 Added: (index_from ~needle:"data-hito-correction-action" page 0));
690 690 Alcotest.(check bool)
691 Added: "the correction action wrapper is absent" true
692 Added: (Option.is_none (index_from ~needle:"correction-actions" page 0));
693 Added: Alcotest.(check bool)
691 694 "a Reset control is present" true
692 695 (Option.is_some
693 696 (index_from ~needle:"data-hito-correction-reset" page 0));
@@ -754,13 +757,36 @@
754 757 now := day 10;
755 758 let _ = register c ~username:"lifter" ~password:"heavyduty1" in
756 759 let _ = start_day_one c in
757 Removed: (* The first-ever Day 1 has no history: no record-to-beat line. *)
760 Added: (* The first-ever Day 1 has no history, but its inputs still show
761 Added: their units and no column heading row. *)
762 Added: let first_page = body (get c "/workout") in
758 763 Alcotest.(check bool)
759 764 "no prior effort on the first workout" true
760 765 (Option.is_none
761 Removed: (index_from ~needle:"exercise-previous"
762 Removed: (body (get c "/workout"))
763 Removed: 0));
766 Added: (index_from ~needle:"exercise-previous" first_page 0));
767 Added: Alcotest.(check bool)
768 Added: "the column heading row is absent" true
769 Added: (Option.is_none
770 Added: (index_from ~needle:"logging-grid-head" first_page 0));
771 Added: Alcotest.(check bool)
772 Added: "nested logging grids are absent" true
773 Added: (Option.is_none (index_from ~needle:"logging-grid" first_page 0)
774 Added: && Option.is_none (index_from ~needle:"ending-block" first_page 0)
775 Added: );
776 Added: Alcotest.(check bool)
777 Added: "the input units are visible" true
778 Added: (Option.is_some (index_from ~needle:">kg<" first_page 0)
779 Added: && Option.is_some (index_from ~needle:">reps<" first_page 0));
780 Added: Alcotest.(check bool)
781 Added: "the ending unit uses an input group" true
782 Added: (match
783 Added: index_from ~needle:"field-extension-amount" first_page 0
784 Added: with
785 Added: | Some ending ->
786 Added: Option.is_some
787 Added: (index_from ~needle:"class=\"input-group\"" first_page
788 Added: ending)
789 Added: | None -> false);
764 790 record_all_day_one_slots c;
765 791 let token = Option.get (csrf_token (body (get c "/workout"))) in
766 792 let _ = post c "/workout/finish" [ ("dream.csrf", token) ] in
@@ -796,10 +822,28 @@
796 822 (Option.is_some
797 823 (index_from ~needle:"exercise-previous" day_one_again 0));
798 824 Alcotest.(check bool)
799 Removed: "the prior load is stated under the load input" true
800 Removed: (Option.is_some (index_from ~needle:"12 kg" day_one_again 0));
825 Added: "the prior load is integrated before the load input" true
826 Added: (match
827 Added: ( index_from ~needle:"exercise-previous" day_one_again 0,
828 Added: index_from ~needle:"name=\"load\"" day_one_again 0 )
829 Added: with
830 Added: | Some previous, Some input -> previous < input
831 Added: | _ -> false);
801 832 Alcotest.(check bool)
802 Removed: "the prior reps are stated under the reps input" true
803 Removed: (Option.is_some (index_from ~needle:"8 reps" day_one_again 0)) );
833 Added: "the prior values contain no unit text" true
834 Added: (Option.is_none (index_from ~needle:"12 kg" day_one_again 0)
835 Added: && Option.is_none (index_from ~needle:"8 reps" day_one_again 0));
836 Added: Alcotest.(check bool)
837 Added: "the prior values use input groups" true
838 Added: (Option.is_some
839 Added: (index_from ~needle:"class=\"input-group" day_one_again 0));
840 Added: Alcotest.(check bool)
841 Added: "the prior reps are integrated before the reps input" true
842 Added: (match
843 Added: ( index_from ~needle:"exercise-previous" day_one_again 0,
844 Added: index_from ~needle:"name=\"reps\"" day_one_again 0 )
845 Added: with
846 Added: | Some previous, Some input -> previous < input
847 Added: | _ -> false) );
804 848 ] );
805 849 ]