[OCaml] High Intensity Training Online
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.
Changed files
ARCHITECTURE.org
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
]