[OCaml] High Intensity Training Online
refactor replace the logging table with a div grid
The set was a semantic table styled as a grid, with thead/tbody/tr set to display:contents to borrow CSS grid. It is a data-entry form, not a data display, so the table semantics and the display:contents hack were a mismatch. Rebuild it as a grid of movement-group divs under a shared pair of column headings; the per-input sr-only labels carry the accessible names. Align the set ending block to the same grid columns so the select and amount line up with the inputs above.
Changed files
lib/presenter/pages_workout.ml
@@ -45,13 +45,15 @@
45
45
[ txt "%s" (errors_to_text [ (field, error) ]) ];
46
46
]
47
47
48
Removed:
(* One movement as a table row: exercise name, load input, reps input. The
49
Removed:
surrounding table gives the Load and Reps column headers. A pre-exhaust pair
50
Removed:
is simply two rows. *)
51
Removed:
let movement_tr ~(errors : (string * string) list) ~name ~load_id ~load_field
48
Added:
(* One movement as a grid group: exercise name, load input, reps input. The
49
Added:
group is a [div] that is [display:contents], so its three cells fall on the
50
Added:
enclosing grid under the shared Load and Reps headings. A pre-exhaust pair is
51
Added:
two such groups. *)
52
Added:
let movement_group ~(errors : (string * string) list) ~name ~load_id ~load_field
52
53
~reps_id ~reps_field ?load_value ?reps_value ?previous ~reps_label () =
53
Removed:
let number_td ~input_id ~field ~value ~step_ ~label =
54
Removed:
tag "td" []
54
Added:
let number_field ~input_id ~field ~value ~step_ ~label =
55
Added:
tag "div"
56
Added:
[ class_ "field field-number" ]
55
57
[
56
58
tag "label"
57
59
[ Dream_html.string_attr "for" "%s" input_id; class_ "sr-only" ]
@@ -77,20 +79,21 @@
77
79
| Some v -> [ Dream_html.string_attr "value" "%s" v ]);
78
80
]
79
81
in
80
Removed:
tag "tr" []
82
Added:
tag "div"
83
Added:
[ class_ "movement" ]
81
84
[
82
Removed:
tag "th"
83
Removed:
[ Dream_html.string_attr "scope" "row"; class_ "exercise-name" ]
85
Added:
tag "div"
86
Added:
[ class_ "exercise-name" ]
84
87
(tag "span" [ class_ "exercise-name-label" ] [ txt "%s" name ]
85
88
::
86
89
(match previous with
87
90
| Some p ->
88
91
[ tag "span" [ class_ "exercise-previous" ] [ txt "Last: %s" p ] ]
89
92
| None -> []));
90
Removed:
number_td ~input_id:load_id ~field:load_field ~value:load_value
93
Added:
number_field ~input_id:load_id ~field:load_field ~value:load_value
91
94
~step_:"0.5" ~label:"Load (kg)";
92
Removed:
number_td ~input_id:reps_id ~field:reps_field ~value:reps_value ~step_:"1"
93
Removed:
~label:reps_label;
95
Added:
number_field ~input_id:reps_id ~field:reps_field ~value:reps_value
96
Added:
~step_:"1" ~label:reps_label;
94
97
]
95
98
96
99
(* [movements] carries the record/correction field spec the controller built
@@ -104,12 +107,12 @@
104
107
let field_id field = Printf.sprintf "slot-%d-%s" slot field in
105
108
let selected = selected_extension in
106
109
let movement_of (m : View_model.Workout.movement) =
107
Removed:
movement_tr ~errors ~name:m.name ~load_id:(field_id m.load_field)
110
Added:
movement_group ~errors ~name:m.name ~load_id:(field_id m.load_field)
108
111
~load_field:m.load_field ~reps_id:(field_id m.reps_field)
109
112
~reps_field:m.reps_field ?load_value:m.load_value ?reps_value:m.reps_value
110
113
?previous:m.previous ~reps_label:m.reps_label ()
111
114
in
112
Removed:
let rows = List.map movement_of movements in
115
Added:
let movement_groups = List.map movement_of movements in
113
116
(* The error rows follow the fields present, so a single-exercise slot reports
114
117
load/reps and a pre-exhaust slot reports the iso/comp fields. *)
115
118
let field_names =
@@ -128,28 +131,26 @@
128
131
Dream_html.csrf_tag request;
129
132
tag "fieldset"
130
133
[ class_ "logging-fieldset" ]
131
Removed:
((* The set as a plain table: one row per movement, with Load and Reps
132
Removed:
column headers. The Ending block is separate, below the table. *)
133
Removed:
tag "table"
134
Removed:
[ class_ "logging-table" ]
135
Removed:
[
136
Removed:
tag "thead" []
137
Removed:
[
138
Removed:
tag "tr" []
139
Removed:
[
140
Removed:
(* Empty header over the exercise-name column: the names are
141
Removed:
self-describing, so the column needs no label. *)
142
Removed:
tag "th" [ Dream_html.string_attr "scope" "col" ] [];
143
Removed:
tag "th"
144
Removed:
[ Dream_html.string_attr "scope" "col" ]
145
Removed:
[ txt "Load (kg)" ];
146
Removed:
tag "th"
147
Removed:
[ Dream_html.string_attr "scope" "col" ]
148
Removed:
[ txt "Reps" ];
149
Removed:
];
150
Removed:
];
151
Removed:
tag "tbody" [] rows;
152
Removed:
]
134
Added:
((* The set as a grid of movement groups under a shared pair of column
135
Added:
headings. Each movement group lays its name, load, and reps onto the
136
Added:
grid. The per-input labels carry the accessible names, so the two
137
Added:
visible headings are decorative. The Ending block is separate,
138
Added:
below the grid. *)
139
Added:
tag "div"
140
Added:
[ class_ "logging-grid" ]
141
Added:
(tag "div"
142
Added:
[
143
Added:
class_ "logging-grid-head";
144
Added:
Dream_html.string_attr "aria-hidden" "true";
145
Added:
]
146
Added:
[
147
Added:
tag "span" [ class_ "logging-grid-head-name" ] [];
148
Added:
tag "span"
149
Added:
[ class_ "logging-grid-head-load" ]
150
Added:
[ txt "Load (kg)" ];
151
Added:
tag "span" [ class_ "logging-grid-head-reps" ] [ txt "Reps" ];
152
Added:
]
153
Added:
:: movement_groups)
153
154
:: tag "div"
154
155
[ class_ "ending-block" ]
155
156
[
lib/web/assets/hito.css
@@ -932,15 +932,12 @@
932
932
.chart-series.reps { stroke: var(--brass); stroke-dasharray: 5 3; }
933
933
934
934
/* --- Workout logging fieldset ---
935
Removed:
The set is a semantic table laid out as a grid so the three columns hold the
936
Removed:
golden ratio: the exercise name is the smaller part (1fr); load and reps take
937
Removed:
the wider φ portion, split evenly (0.809fr each). thead/tbody/tr are
938
Removed:
display:contents so their cells fall on the grid. The set ending (extension
939
Removed:
select + optional amount) is a separate flex row below the table. *)
940
Removed:
.logging-fieldset {
941
Removed:
border-top: 1px solid var(--rule);
942
Removed:
}
943
Removed:
935
Added:
The set is a grid of movement groups so the three columns hold the golden
936
Added:
ratio: the exercise name is the smaller part (1fr); load and reps take the
937
Added:
wider φ portion, split evenly (0.809fr each). Each movement group and the
938
Added:
heading row are display:contents so their cells fall on the grid. The set
939
Added:
ending (extension select + optional amount) is a separate row below the
940
Added:
grid. *)
944
941
.exercise-name-label {
945
942
display: block;
946
943
color: var(--ink);
@@ -957,7 +954,7 @@
957
954
font-variant-numeric: tabular-nums;
958
955
}
959
956
960
Removed:
.logging-table {
957
Added:
.logging-grid {
961
958
display: grid;
962
959
/* Golden ratio between the name column and the two number columns: name is
963
960
the smaller part (1), load and reps together are the larger part (φ ≈
@@ -967,45 +964,54 @@
967
964
gap: 0.35rem 0.6rem;
968
965
margin-top: 1rem;
969
966
}
970
Removed:
.logging-table thead,
971
Removed:
.logging-table tbody,
972
Removed:
.logging-table tr {
967
Added:
/* The heading row and each movement group flatten onto the grid so their cells
968
Added:
land in the three columns. */
969
Added:
.logging-grid-head,
970
Added:
.movement {
973
971
display: contents;
974
972
}
975
Removed:
.logging-table th {
976
Removed:
margin: 0;
977
Removed:
padding: 0;
978
Removed:
text-align: left;
979
Removed:
}
980
Removed:
.logging-table thead th {
973
Added:
/* The two visible column headings sit over the number columns; the name column
974
Added:
has no heading (the names are self-describing). */
975
Added:
.logging-grid-head-load,
976
Added:
.logging-grid-head-reps {
981
977
padding-bottom: 0.35rem;
982
978
color: var(--muted-ink);
983
979
font: 700 var(--font-size-small)/1 var(--sans);
984
980
align-self: end;
985
981
}
986
Removed:
.logging-table td { padding: 0; }
987
Removed:
.logging-table input { width: 100%; }
982
Added:
.logging-grid .field-number { padding: 0; }
983
Added:
.logging-grid input { width: 100%; }
988
984
989
Removed:
/* The set ending: the extension select and its optional amount on one line.
990
Removed:
The select flexes to fill; the amount is a compact fixed width followed by a
991
Removed:
muted unit. The amount hides at positive failure (nothing to count). *)
992
Removed:
/* The set ending: the extension select and its optional amount on one line, as
993
Removed:
a two-column grid (select fills, amount sizes to content). A grid is
994
Removed:
deterministic here where flex min-content sizing proved fragile. *)
985
Added:
/* The set ending sits under the number columns and shares the logging grid's
986
Added:
geometry, so the select's left edge lines up with the Load column and the
987
Added:
amount lines up with the Reps column. The name column stays empty. The select
988
Added:
fills its column; the amount is a compact input followed by a muted unit. */
995
989
.ending-block {
996
990
display: grid;
997
Removed:
grid-template-columns: 1fr auto;
991
Added:
grid-template-columns: minmax(0, 1fr) minmax(0, 0.809fr) minmax(0, 0.809fr);
998
992
align-items: end;
999
993
gap: 0.6rem;
1000
Removed:
margin-top: 0.9rem;
994
Added:
margin-top: 0.6rem;
1001
995
}
1002
996
.ending-block .field { margin: 0; min-width: 0; }
997
Added:
/* The select sits in the Load column and the amount in the Reps column, so both
998
Added:
align with the inputs above. At positive failure the amount is hidden, so the
999
Added:
select then spans both number columns to fill that width (see the :has block). */
1000
Added:
.ending-block .field-ending { grid-column: 2; }
1001
Added:
.ending-block .field-extension-amount { grid-column: 3; }
1002
Added:
/* Input and its unit stay together on the amount line, and never wrap onto
1003
Added:
separate lines. */
1003
1004
.ending-block .field-extension-amount {
1004
1005
display: flex;
1005
1006
align-items: baseline;
1006
1007
gap: 0.35rem;
1008
Added:
flex-wrap: nowrap;
1009
Added:
white-space: nowrap;
1007
1010
}
1008
Removed:
.ending-block .field-extension-amount input { width: 4.5rem; }
1011
Added:
.ending-block .field-extension-amount input {
1012
Added:
width: 4.5rem;
1013
Added:
flex: none;
1014
Added:
}
1009
1015
.ending-block .field-unit {
1010
1016
color: var(--muted-ink);
1011
1017
font: 500 var(--font-size-small)/1 var(--sans);
@@ -1019,6 +1025,12 @@
1019
1025
.logging-fieldset:has(select[name="extension"] option[value=""]:checked)
1020
1026
.field-extension-amount {
1021
1027
display: none;
1028
Added:
}
1029
Added:
/* With no amount, the select fills both number columns to match the inputs. */
1030
Added:
.logging-fieldset:has(select[name="extension"] option[value=""]:checked)
1031
Added:
.ending-block
1032
Added:
.field-ending {
1033
Added:
grid-column: 2 / 4;
1022
1034
}
1023
1035
.logging-fieldset:has(select[name="extension"] option[value="static"]:checked)
1024
1036
.amount-label-reps {