refactor lay the logging fieldset out in one grid

The workout logging fieldset built a separate CSS grid per movement row plus another for the column-heading row. The rows aligned only because each grid repeated the same track sizes; the exercise name, load, and reps columns were not part of one grid. Wrap the whole fieldset in a single [.logging-grid]. Each movement's cells join that grid directly through a [display: contents] wrapper, so every name, load, and reps column lines up across the fieldset. The Ending control spans the full width below the movement rows. No doctrine bearing: this is presentation only. Plan and record stay distinct and no training rule changes. Update the web flow test to assert the single shared grid replaces the per-row exercise-fields layout.

Commit
6b62f25500c6b47ab34b021c76a070796515881a
Author
Marius Peter <dev@marius-peter.com>
Author date
Committer
Marius Peter <dev@marius-peter.com>
Committer date
Changed files
lib/web/assets/hito.css
index 5b575367..cf87cf8d 100644..100644
@@ -347,31 +347,35 @@
347 347 .button-group form { margin: 0; }
348 348 .button-group input[type="submit"] { width: 100%; }
349 349
350 Removed: /* A movement's fields as one compact grid row: exercise name, load, reps. On
351 Removed: mobile all three sit on one line — the name takes the remaining space and the
352 Removed: two numeric inputs take fixed, tap-friendly columns — so a set fits without
353 Removed: scrolling. The [.field] top margin is dropped here; the grid handles spacing.
354 Removed: The name column spans the full width above the inputs only when it would
355 Removed: otherwise be too cramped, at the narrowest widths. */
356 Removed: .exercise-fields {
350 Added: /* One responsive grid for the whole logging fieldset. Every movement's name,
351 Added: load, and reps line up in the same three columns — the name takes the
352 Added: remaining space, the two numeric inputs take fixed, tap-friendly columns — so
353 Added: a set fits on one line without scrolling. A [.movement] wrapper groups a
354 Added: movement's cells for the markup but opens no nested grid ([display:
355 Added: contents]), so all rows share this one grid. The [.field] top margin is
356 Added: dropped here; the grid gap handles spacing. */
357 Added: .logging-grid {
357 358 display: grid;
358 359 grid-template-columns: minmax(0, 1fr) 5.5rem 5.5rem;
359 360 align-items: end;
360 Removed: gap: 0.6rem;
361 Added: gap: 0.6rem 0.6rem;
361 362 margin-top: 1rem;
362 363 }
363 Removed: .exercise-fields .exercise-name { margin: 0; align-self: center; }
364 Removed: .exercise-fields .field { margin-top: 0; }
364 Added: .movement { display: contents; }
365 Added: .logging-grid .exercise-name { margin: 0; align-self: center; }
366 Added: .logging-grid .field { margin-top: 0; }
367 Added: /* The Ending control spans the full width below the movement rows. */
368 Added: .logging-grid .field-ending { grid-column: 1 / -1; margin-top: 0.4rem; }
365 369
366 370 /* A shared heading row: the Load and Reps column headings shown once above a
367 Removed: multi-exercise group, so the labels are not repeated above every input. */
368 Removed: .exercise-fields-head { margin-top: 1rem; align-items: end; }
371 Added: multi-exercise group, so the labels are not repeated above every input. The
372 Added: cells sit in the same grid columns as the movement inputs below them. */
369 373 .col-heading {
370 374 color: var(--ink);
371 375 font-size: var(--font-size-small);
372 376 font-weight: 700;
377 Added: align-self: end;
373 378 }
374 Removed: .exercise-fields-head + .exercise-fields { margin-top: 0.4rem; }
375 379
376 380 /* Visually hidden but available to assistive technology: the per-input labels
377 381 stay for screen readers while the visible headings carry the column names. */
lib/web/pages.ml
index 792a1ffa..4ece0c41 100644..100644
@@ -506,15 +506,17 @@
506 506 | Some v -> [ Dream_html.string_attr "value" "%s" v ]);
507 507 ]
508 508
509 Removed: (* A movement's fields as one compact grid row: the exercise name, the load
510 Removed: input, and the reps input side by side. On mobile the [.exercise-fields] grid
511 Removed: keeps all three on one line so a set fits without scrolling; it widens on
512 Removed: larger screens. The name reads as a heading, the inputs as labelled fields. *)
509 Added: (* A movement's fields as cells of the fieldset grid: the exercise name, the
510 Added: load input, and the reps input. The cells are direct participants of the one
511 Added: [.logging-grid] the fieldset wraps them in — a [display: contents] wrapper
512 Added: groups a movement without opening a nested grid — so every movement's name,
513 Added: load, and reps columns line up across the whole fieldset. The name reads as a
514 Added: heading, the inputs as labelled fields. *)
513 515 let movement_row ~(errors : (string * string) list) ~name ~load_id ~load_field
514 516 ~reps_id ~reps_field ?load_value ?reps_value ~reps_label
515 517 ?(hide_labels = false) () =
516 518 tag "div"
517 Removed: [ class_ "exercise-fields" ]
519 Added: [ class_ "movement" ]
518 520 [
519 521 tag "p" [ class_ "exercise-name" ] [ txt "%s" name ];
520 522 input_row ?value:load_value ~placeholder:"kg" ~hide_label:hide_labels
@@ -526,11 +528,11 @@
526 528 ]
527 529
528 530 (* A shared heading row for a multi-exercise group: empty name cell, then the
529 Removed: Load and Reps column headings. Rendered once above the movement rows so the
530 Removed: labels are not repeated above every input. *)
531 Added: Load and Reps column headings. Rendered once as cells of the fieldset grid so
532 Added: the labels are not repeated above every input. *)
531 533 let exercise_fields_head ~reps_label () =
532 534 tag "div"
533 Removed: [ class_ "exercise-fields exercise-fields-head" ]
535 Added: [ class_ "movement movement-head" ]
534 536 [
535 537 tag "span" [ class_ "exercise-name" ] [ txt "" ];
536 538 tag "span" [ class_ "col-heading" ] [ txt "Load (kg)" ];
@@ -610,26 +612,34 @@
610 612 Dream_html.csrf_tag request;
611 613 tag "fieldset"
612 614 [ class_ "logging-fieldset" ]
613 Removed: (fields
614 Removed: @ [
615 Removed: tag "label"
616 Removed: [ Dream_html.string_attr "for" "%s" (field_id "extension") ]
617 Removed: [ txt "Ending" ];
618 Removed: extension_select ~selected
619 Removed: ~invalid:(List.mem_assoc "extension" errors)
620 Removed: ~input_id:(field_id "extension") ();
621 Removed: ]
622 Removed: @ List.concat_map
623 Removed: (fun field -> error_for ~input_id:(field_id field) field errors)
624 Removed: [
625 Removed: "load";
626 Removed: "reps";
627 Removed: "iso_load";
628 Removed: "iso_reps";
629 Removed: "comp_load";
630 Removed: "comp_reps";
631 Removed: "extension";
632 Removed: ]
615 Added: (tag "div"
616 Added: [ class_ "logging-grid" ]
617 Added: (fields
618 Added: @ [
619 Added: tag "div"
620 Added: [ class_ "field field-ending" ]
621 Added: [
622 Added: tag "label"
623 Added: [
624 Added: Dream_html.string_attr "for" "%s" (field_id "extension");
625 Added: ]
626 Added: [ txt "Ending" ];
627 Added: extension_select ~selected
628 Added: ~invalid:(List.mem_assoc "extension" errors)
629 Added: ~input_id:(field_id "extension") ();
630 Added: ];
631 Added: ])
632 Added: :: List.concat_map
633 Added: (fun field -> error_for ~input_id:(field_id field) field errors)
634 Added: [
635 Added: "load";
636 Added: "reps";
637 Added: "iso_load";
638 Added: "iso_reps";
639 Added: "comp_load";
640 Added: "comp_reps";
641 Added: "extension";
642 Added: ]
633 643 @ [
634 644 void "input"
635 645 [ type_ "submit"; Dream_html.string_attr "value" "%s" submit ];
test/test_web.ml
index 9c9a4ee9..fd69ed2a 100644..100644
@@ -512,7 +512,7 @@
512 512 Alcotest.(check bool)
513 513 "shows bottom navigation" true
514 514 (contains ~substring:"bottom-nav" page) );
515 Removed: ( "effort forms lay name, load, and reps out in one compact row",
515 Added: ( "effort forms lay the whole fieldset out in one responsive grid",
516 516 `Quick,
517 517 fun () ->
518 518 let c = client () in
@@ -525,7 +525,13 @@
525 525 in
526 526 let page = body (get c "/workout") in
527 527 Alcotest.(check bool)
528 Removed: "uses the compact exercise-fields grid" true
528 Added: "wraps the fieldset in one shared grid" true
529 Added: (contains ~substring:"class=\"logging-grid\"" page);
530 Added: Alcotest.(check bool)
531 Added: "groups a movement's cells without a nested grid" true
532 Added: (contains ~substring:"class=\"movement\"" page);
533 Added: Alcotest.(check bool)
534 Added: "no longer uses a per-row exercise-fields grid" false
529 535 (contains ~substring:"class=\"exercise-fields\"" page);
530 536 Alcotest.(check bool)
531 537 "still labels load" true