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.

Commit
b092e4aabebaa739318d40aaec0d695d1084c1fe
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 7f133327..678ec7ba 100644..100644
@@ -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
index ee832f73..6fa4c9a9 100644..100644
@@ -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 {