feat make the logbook an accordion of workouts

Replace the logbook's workout table with a <details> accordion, one collapsible entry per workout under the arrow-paginated month calendar. Each summary shows the monospace date and the workout title; the open panel lists the exercises performed with their progress deltas and, for a native workout, tucks the Edit and Delete actions inside. An import keeps its blood "Imported" pill and stays read-only. Duration is no longer shown. The panel unfolds with the same ::details-content grid animation the workout-logging slot accordion uses, so opening a logged workout and opening a logging slot feel identical. Reduced-motion users snap. Calendar table headers drop the uppercase grey treatment for plain black bold, matching the rest of the app.

Commit
8f2a67d3ffe9f85d7b156d44b66b09228a6aaceb
Author
Marius Peter <dev@marius-peter.com>
Author date
Committer
Marius Peter <dev@marius-peter.com>
Committer date
Changed files
lib/presenter/pages.mli
index 449e8f23..bcb01521 100644..100644
@@ -69,12 +69,16 @@
69 69 ?expanded_record:View_model.Logbook_entry.t ->
70 70 View_model.Logbook_entry.t list ->
71 71 page
72 Removed: (** The logbook: recorded workouts and a modal subjective feedback form that
73 Removed: shows every factor at once, each optionally rated against the trainee's
74 Removed: usual, with one submit. [calendar] is the current month above the table,
75 Removed: highlighting days a workout was performed. [suggest_feedback] shows a prompt
76 Removed: inviting feedback, set after a workout finishes. [feedback_open] renders the
77 Removed: modal open for no-script clients. *)
72 Added: (** The logbook: one month of recorded workouts as an accordion, one collapsible
73 Added: entry each, and a modal subjective feedback form that shows every factor at
74 Added: once, each optionally rated against the trainee's usual, with one submit.
75 Added: [calendar] is the current month above the accordion, highlighting days a
76 Added: workout was performed; its arrows page through months and Today returns to
77 Added: the present month. Each entry's summary is its date and title; its open
78 Added: panel lists the exercises performed and, for a native workout, the Edit and
79 Added: Delete actions. [suggest_feedback] shows a prompt inviting feedback, set
80 Added: after a workout finishes. [feedback_open] renders the modal open for
81 Added: no-script clients. *)
78 82
79 83 val progress :
80 84 Dream.request ->
lib/presenter/pages_logbook.ml
index f880aaef..3c3a07d1 100644..100644
@@ -171,82 +171,76 @@
171 171 exercise_block ~name:e.exercise_name ~weight:e.load ~reps:e.reps
172 172 ~prev_weight:e.previous_load ~prev_reps:e.previous_reps
173 173
174 Removed: (* The three-dot menu shared shape: a summary button opening a drawer. *)
175 Removed: let row_menu ~aria_summary ~aria_drawer ~extra_class contents =
176 Removed: tag "details"
177 Removed: [ class_ "logbook-row-menu" ]
178 Removed: [
179 Removed: tag "summary"
180 Removed: [ Dream_html.string_attr "aria-label" "%s" aria_summary ]
181 Removed: [ txt "⋮" ];
182 Removed: tag "div"
183 Removed: [
184 Removed: Dream_html.string_attr "class" "logbook-row-menu-drawer %s"
185 Removed: extra_class;
186 Removed: Dream_html.string_attr "role" "menu";
187 Removed: Dream_html.string_attr "aria-label" "%s" aria_drawer;
188 Removed: ]
189 Removed: contents;
190 Removed: ]
191 Removed:
192 Removed: (* An imported workout's menu: an "Imported" pill, then the per-exercise efforts
193 Removed: and progress since each was last performed. Read-only; no edit or delete. The
194 Removed: pill lives here in the drawer, not beside the title. *)
195 Removed: let imported_menu (entry : View_model.Logbook_entry.t) =
196 Removed: row_menu
197 Removed: ~aria_summary:
198 Removed: (Printf.sprintf "Imported exercises for %s on %s" entry.workout_name
199 Removed: entry.date)
200 Removed: ~aria_drawer:"Imported exercises" ~extra_class:"logbook-import-drawer"
201 Removed: [
202 Removed: tag "p"
203 Removed: [ class_ "logbook-import-pill-row" ]
204 Removed: [ tag "span" [ class_ "logbook-import-pill" ] [ txt "Imported" ] ];
205 Removed: tag "ul"
206 Removed: [ class_ "logbook-change-list" ]
207 Removed: (List.map imported_effort_line
208 Removed: entry.View_model.Logbook_entry.imported_efforts);
209 Removed: ]
210 Removed:
211 Removed: (* A native workout's menu: a colour-coded per-exercise progress summary, then
212 Removed: the Edit and Delete actions. *)
213 Removed: let native_menu request (entry : View_model.Logbook_entry.t) =
214 Removed: ignore request;
215 Removed: let progress =
216 Removed: if entry.changes = [] then
217 Removed: [ tag "p" [ class_ "logbook-no-progress" ] [ txt "No prior comparison" ] ]
218 Removed: else
174 Added: (* One workout as an accordion entry. The summary carries the monospace date and
175 Added: the workout title; the open panel lists the exercises performed and, for a
176 Added: native workout, the Edit and Delete actions tucked inside. An import is
177 Added: read-only: it shows the "Imported" pill and its efforts, with no actions. A
178 Added: native workout with no prior comparison still lists nothing to compare and
179 Added: keeps its actions. *)
180 Added: let workout_accordion request (entry : View_model.Logbook_entry.t) =
181 Added: let summary =
182 Added: tag "summary"
183 Added: [ class_ "logbook-entry-summary" ]
184 Added: ([
185 Added: tag "span" [ class_ "logbook-entry-date" ] [ txt "%s" entry.date ];
186 Added: tag "span"
187 Added: [ class_ "logbook-entry-title" ]
188 Added: [ txt "%s" entry.workout_name ];
189 Added: ]
190 Added: @
191 Added: if entry.imported then
192 Added: [ tag "span" [ class_ "logbook-import-pill" ] [ txt "Imported" ] ]
193 Added: else [])
194 Added: in
195 Added: let panel_contents =
196 Added: if entry.imported then
219 197 [
220 198 tag "ul"
221 199 [ class_ "logbook-change-list" ]
222 Removed: (List.map native_change_line entry.changes);
223 Removed: tag "hr" [ class_ "logbook-menu-rule" ] [];
200 Added: (List.map imported_effort_line
201 Added: entry.View_model.Logbook_entry.imported_efforts);
224 202 ]
225 Removed: in
226 Removed: row_menu
227 Removed: ~aria_summary:
228 Removed: (Printf.sprintf "Actions for %s on %s" entry.workout_name entry.date)
229 Removed: ~aria_drawer:"Workout actions" ~extra_class:"logbook-import-drawer"
230 Removed: (progress
231 Removed: @ [
232 Removed: tag "a"
203 Added: else
204 Added: let exercises =
205 Added: if entry.changes = [] then
233 206 [
234 Removed: href Routes.record entry.id;
235 Removed: Dream_html.attr "data-hito-app-link";
236 Removed: Dream_html.string_attr "role" "menuitem";
207 Added: tag "p"
208 Added: [ class_ "logbook-no-progress" ]
209 Added: [ txt "No prior comparison" ];
237 210 ]
238 Removed: [ txt "Edit" ];
239 Removed: tag "a"
211 Added: else
240 212 [
241 Removed: Dream_html.string_attr "href" "/logbook?delete=%s" entry.id;
242 Removed: Dream_html.string_attr "role" "menuitem";
243 Removed: class_ "danger";
213 Added: tag "ul"
214 Added: [ class_ "logbook-change-list" ]
215 Added: (List.map native_change_line entry.changes);
244 216 ]
245 Removed: [ txt "Delete" ];
246 Removed: ])
217 Added: in
218 Added: exercises
219 Added: @ [
220 Added: tag "div"
221 Added: [ class_ "logbook-entry-actions" ]
222 Added: [
223 Added: tag "a"
224 Added: [
225 Added: href Routes.record entry.id;
226 Added: class_ "button secondary";
227 Added: Dream_html.attr "data-hito-app-link";
228 Added: ]
229 Added: [ txt "Edit" ];
230 Added: tag "a"
231 Added: [
232 Added: Dream_html.string_attr "href" "/logbook?delete=%s" entry.id;
233 Added: class_ "button outline danger";
234 Added: ]
235 Added: [ txt "Delete" ];
236 Added: ];
237 Added: ]
238 Added: in
239 Added: ignore request;
240 Added: tag "details"
241 Added: [ class_ "logbook-entry" ]
242 Added: (summary :: [ tag "div" [ class_ "logbook-entry-panel" ] panel_contents ])
247 243
248 Removed: (* A single month as a week grid. Days that carry a workout are highlighted;
249 Removed: today is marked. Rendered above the logbook table. *)
250 244 let calendar_month_name month =
251 245 List.nth
252 246 [
@@ -265,6 +259,10 @@
265 259 ]
266 260 (month - 1)
267 261
262 Added: (* The month calendar above the accordion. Days that carry a workout are
263 Added: highlighted; today is marked. A header row holds a previous arrow, the month
264 Added: and year, a Today link, and a next arrow. The arrows page the query, and
265 Added: Today returns to the current month. *)
268 266 let calendar_grid (cal : View_model.Calendar.t) =
269 267 let cell = function
270 268 | View_model.Calendar.Blank -> tag "td" [ class_ "calendar-blank" ] []
@@ -300,7 +298,8 @@
300 298 ]
301 299 [ txt "%s" label ]
302 300 in
303 Removed: (* Header: previous, a Today link that returns to the current month, next. *)
301 Added: (* Header: previous, the month and year, a Today link that returns to the
302 Added: current month, next. *)
304 303 tag "div"
305 304 [ class_ "calendar" ]
306 305 [
@@ -349,8 +348,8 @@
349 348 html_page ~viewer ~request ~active:"logbook" ?workout_started "Logbook"
350 349 ([
351 350 tag "h1" [] [ txt "Logbook" ];
352 Removed: (* Actions above the ledger, so they are reachable without scrolling past
353 Removed: a long table. View progress opens the per-exercise chart; Record
351 Added: (* Actions above the accordion, so they are reachable without scrolling
352 Added: past a long month. View progress opens the per-exercise chart; Record
354 353 feedback opens the subjective-feedback modal. *)
355 354 tag "div"
356 355 [ class_ "button-group logbook-actions-bar" ]
@@ -380,58 +379,13 @@
380 379 @ [
381 380 (if records = [] then tag "p" [] [ txt "Nothing logged yet." ]
382 381 else
383 Removed: tag "table"
384 Removed: [ class_ "logbook-table" ]
385 Removed: [
386 Removed: tag "thead" []
387 Removed: [
388 Removed: tag "tr" []
389 Removed: [
390 Removed: tag "th"
391 Removed: [ Dream_html.string_attr "scope" "col" ]
392 Removed: [ txt "Date" ];
393 Removed: tag "th"
394 Removed: [ Dream_html.string_attr "scope" "col" ]
395 Removed: [ txt "Workout" ];
396 Removed: tag "th"
397 Removed: [ Dream_html.string_attr "scope" "col" ]
398 Removed: [ txt "Duration" ];
399 Removed: tag "th"
400 Removed: ([ class_ "logbook-actions-heading" ]
401 Removed: @ [ Dream_html.string_attr "scope" "col" ])
402 Removed: [ tag "span" [ class_ "sr-only" ] [ txt "Actions" ] ];
403 Removed: ];
404 Removed: ];
405 Removed: tag "tbody" []
406 Removed: (List.map
407 Removed: (fun (entry : View_model.Logbook_entry.t) ->
408 Removed: (* The workout name renders plainly. An import is
409 Removed: marked by the oxblood pill inside its three-dot
410 Removed: menu drawer, and the menu also carries the
411 Removed: colour-coded per-exercise progress; a native
412 Removed: menu carries Edit and Delete. *)
413 Removed: let name_cell =
414 Removed: tag "td" [] [ txt "%s" entry.workout_name ]
415 Removed: in
416 Removed: let menu =
417 Removed: if entry.imported then imported_menu entry
418 Removed: else native_menu request entry
419 Removed: in
420 Removed: tag "tr" []
421 Removed: [
422 Removed: tag "td" [] [ txt "%s" entry.date ];
423 Removed: name_cell;
424 Removed: tag "td"
425 Removed: [ class_ "logbook-duration" ]
426 Removed: [ txt "%s" entry.duration ];
427 Removed: tag "td" [ class_ "logbook-actions" ] [ menu ];
428 Removed: ])
429 Removed: records);
430 Removed: ]);
382 Added: tag "div"
383 Added: [ class_ "logbook-entries" ]
384 Added: (List.map (workout_accordion request) records));
431 385 ]
432 386 (* The subjective-feedback modal stays in the DOM so the Record feedback
433 Removed: button opens it. The delete-confirm dialog appears only when a row asked
434 Removed: for it. *)
387 Added: button opens it. The delete-confirm dialog appears only when an entry
388 Added: asked for it. *)
435 389 @ [ feedback_modal request ~open_:feedback_open ]
436 390 @
437 391 match delete_record with
lib/web/assets/hito.css
index 8ce45f5e..92920cb0 100644..100644
@@ -613,143 +613,93 @@
613 613 background: var(--blood-hover);
614 614 }
615 615
616 Removed: /* The logbook is one real table at every width. The columns are date, workout
617 Removed: name, duration, and the row menu. They are few and narrow enough to share a
618 Removed: single table layout, so there is no mobile card reflow. The name column
619 Removed: absorbs the spare width and truncates when space is tight. */
620 Removed: .logbook-table {
621 Removed: width: 100%;
616 Added: /* The logbook is an accordion: one collapsible entry per workout under the
617 Added: month calendar. A summary shows the monospace date and the workout title; the
618 Added: open panel lists the exercises performed and, for a native workout, the Edit
619 Added: and Delete actions. */
620 Added: .logbook-entries {
621 Added: display: grid;
622 Added: gap: 0.4rem;
622 623 margin-top: 1.5rem;
623 Removed: border-collapse: collapse;
624 Removed: table-layout: fixed;
625 624 }
626 Removed: .logbook-table th,
627 Removed: .logbook-table td {
628 Removed: padding: 0.55rem 0.6rem;
625 Added: .logbook-entry {
629 626 border-bottom: 1px solid var(--rule);
630 Removed: text-align: left;
631 Removed: vertical-align: baseline;
632 627 }
633 Removed: .logbook-table th {
628 Added: .logbook-entry-summary {
629 Added: display: flex;
630 Added: align-items: baseline;
631 Added: gap: 0.75rem;
632 Added: padding: 0.6rem 0.2rem;
633 Added: cursor: pointer;
634 Added: list-style: none;
635 Added: }
636 Added: .logbook-entry-summary::-webkit-details-marker {
637 Added: display: none;
638 Added: }
639 Added: /* A rotating caret marks the open state, drawn before the date. */
640 Added: .logbook-entry-summary::before {
641 Added: content: "›";
642 Added: flex: 0 0 auto;
634 643 color: var(--muted-ink);
635 Removed: font: 700 var(--font-size-small)/1 var(--sans);
636 Removed: letter-spacing: 0.04em;
637 Removed: text-transform: uppercase;
644 Added: font-size: 1.1rem;
645 Added: line-height: 1;
646 Added: transition: transform 220ms ease;
638 647 }
639 Removed: /* The treatment for each column: a monospaced date, a sans workout name that
640 Removed: absorbs the spare width and truncates, a monospaced duration, and the
641 Removed: three-dot menu. */
642 Removed: .logbook-table td:nth-child(1) {
648 Added: .logbook-entry[open] > .logbook-entry-summary::before {
649 Added: transform: rotate(90deg);
650 Added: }
651 Added: /* Unfold and collapse, matching the workout-logging slot accordion.
652 Added: ::details-content is the box the browser toggles for a <details>. Drive a
653 Added: grid row from 0fr to 1fr off the [open] attribute so both opening and closing
654 Added: ease. [visibility] keeps a closed panel out of the tab order and the
655 Added: accessibility tree. Browsers without ::details-content, and reduced-motion
656 Added: users, simply snap. */
657 Added: .logbook-entry::details-content {
658 Added: content-visibility: visible;
659 Added: display: grid;
660 Added: grid-template-rows: 0fr;
661 Added: visibility: hidden;
662 Added: transition:
663 Added: grid-template-rows 220ms ease,
664 Added: visibility 220ms;
665 Added: }
666 Added: .logbook-entry[open]::details-content {
667 Added: grid-template-rows: 1fr;
668 Added: visibility: visible;
669 Added: }
670 Added: .logbook-entry-date {
671 Added: flex: 0 0 auto;
643 672 font-family: var(--mono);
673 Added: color: var(--muted-ink);
644 674 white-space: nowrap;
645 675 }
646 Removed: .logbook-table td:nth-child(2) {
647 Removed: width: 100%;
648 Removed: max-width: 0;
676 Added: .logbook-entry-title {
677 Added: flex: 1 1 auto;
649 678 font-family: var(--sans);
679 Added: font-weight: 700;
650 680 overflow: hidden;
651 681 text-overflow: ellipsis;
652 682 white-space: nowrap;
653 683 }
654 Removed: .logbook-table td:nth-child(3) {
655 Removed: font-family: var(--mono);
656 Removed: color: var(--muted-ink);
657 Removed: white-space: nowrap;
684 Added: .logbook-entry-panel {
685 Added: min-height: 0;
686 Added: overflow: clip;
687 Added: padding: 0.25rem 0.2rem 1rem 1.6rem;
658 688 }
659 Removed:
660 Removed: @media (max-width: 30rem) {
661 Removed: .logbook-table th,
662 Removed: .logbook-table td {
663 Removed: padding-inline: 0.25rem;
664 Removed: }
665 Removed:
666 Removed: .logbook-actions-heading,
667 Removed: .logbook-actions {
668 Removed: width: 2.5rem;
669 Removed: }
670 Removed:
671 Removed: .logbook-row-menu > summary {
672 Removed: width: 2.5rem;
673 Removed: }
689 Added: /* The Edit and Delete affordances sit inside the open panel, after the exercise
690 Added: list. Delete is an outline danger button; Edit a secondary. */
691 Added: .logbook-entry-actions {
692 Added: display: flex;
693 Added: gap: 0.75rem;
694 Added: margin-top: 1rem;
674 695 }
675 Removed: .logbook-duration {
676 Removed: white-space: nowrap;
677 Removed: font-family: var(--mono);
678 Removed: }
679 696 .logbook-no-progress { color: var(--muted-ink); }
680 Removed:
681 Removed: /* One native <details> action menu per row. The dots keep the ledger compact.
682 Removed: The drawer holds the destructive actions outside the evidence fields. */
683 Removed: .logbook-actions-heading,
684 Removed: .logbook-actions {
685 Removed: width: 2.75rem;
686 Removed: text-align: right;
697 Added: @media (prefers-reduced-motion: reduce) {
698 Added: .logbook-entry::details-content { transition: none; }
687 699 }
688 Removed: .logbook-row-menu {
689 Removed: position: relative;
690 Removed: display: inline-block;
691 Removed: }
692 Removed: .logbook-row-menu > summary {
693 Removed: display: grid;
694 Removed: place-items: center;
695 Removed: width: 2.75rem;
696 Removed: height: 2.75rem;
697 Removed: border-radius: 50%;
698 Removed: color: var(--ink);
699 Removed: cursor: pointer;
700 Removed: list-style: none;
701 Removed: font-size: 1.35rem;
702 Removed: line-height: 1;
703 Removed: }
704 Removed: .logbook-row-menu > summary:hover,
705 Removed: .logbook-row-menu[open] > summary { background: var(--brass-wash); }
706 Removed: .logbook-row-menu-drawer {
707 Removed: position: absolute;
708 Removed: right: 0;
709 Removed: z-index: 5;
710 Removed: display: flex;
711 Removed: flex-direction: column;
712 Removed: min-width: 8rem;
713 Removed: padding: 0.3rem;
714 Removed: border: 1px solid var(--rule-strong);
715 Removed: border-radius: var(--radius);
716 Removed: background: var(--paper-raised);
717 Removed: box-shadow: 0 0.5rem 1.25rem rgba(0, 0, 0, 0.18);
718 Removed: }
719 Removed: .logbook-row-menu[open] > .logbook-row-menu-drawer {
720 Removed: animation: profile-menu-open 150ms ease both;
721 Removed: }
722 Removed: .logbook-row-menu-drawer a {
723 Removed: min-height: 2.5rem;
724 Removed: padding: 0.5rem 0.7rem;
725 Removed: border-radius: var(--radius);
726 Removed: color: var(--ink);
727 Removed: font: inherit;
728 Removed: font-weight: 700;
729 Removed: text-decoration: none;
730 Removed: }
731 Removed: .logbook-row-menu-drawer a:hover { background: var(--hover-surface); }
732 Removed: .logbook-row-menu-drawer a.danger { color: var(--blood-dark); }
733 Removed: .logbook-row-menu-drawer a.danger:hover {
734 Removed: color: var(--on-primary);
735 Removed: background: var(--blood);
736 Removed: }
737 700
738 Removed: /* The imported workout's menu drawer holds an exercise and progress list
739 Removed: instead of action links. The drawer is therefore wider, and a reader reads
740 Removed: its items instead of clicking them. */
741 Removed: .logbook-import-drawer {
742 Removed: min-width: 15rem;
743 Removed: max-width: min(22rem, 80vw);
744 Removed: padding: 0.6rem 0.8rem;
745 Removed: text-align: left;
746 Removed: }
747 Removed:
748 Removed: /* A blood outline "Imported" pill marks an import. The pill sits at the
749 Removed: top of the three-dot menu drawer, not beside the title. */
750 Removed: .logbook-import-pill-row {
751 Removed: margin: 0 0 0.5rem;
752 Removed: }
701 Added: /* A blood outline "Imported" pill marks an import, shown in the entry summary
702 Added: beside the title. */
753 703 .logbook-import-pill {
754 704 display: inline-block;
755 705 padding: 0.05rem 0.5rem;
@@ -800,10 +750,11 @@
800 750 /* The action bar under the logbook heading. */
801 751 .logbook-actions-bar { margin: 0.5rem 0 1rem; }
802 752
803 Removed: /* The month calendar above the logbook is a self-contained widget, flat on
804 Removed: the page. A header row holds a previous arrow, a month and year picker, and
805 Removed: a next arrow. Below it sit a Today link and the seven-column day grid. Days
806 Removed: with a workout are filled blood. Today is ringed. */
753 Added: /* The month calendar is a self-contained widget above the accordion, flat on
754 Added: the page. A header row holds a previous arrow, the month and year, a Today
755 Added: link, and a next arrow. Below it sits the seven-column day grid. The arrows
756 Added: page through months and Today returns to the present. Days with a workout are
757 Added: filled blood. Today is ringed. */
807 758 .calendar {
808 759 max-width: 22rem;
809 760 margin: 0 auto 1.5rem;
@@ -814,6 +765,12 @@
814 765 align-items: center;
815 766 gap: 0.4rem;
816 767 }
768 Added: .calendar-month {
769 Added: flex: 1 1 auto;
770 Added: text-align: center;
771 Added: font-weight: 700;
772 Added: color: var(--ink);
773 Added: }
817 774 .calendar-arrow {
818 775 display: inline-flex;
819 776 align-items: center;
@@ -836,7 +793,7 @@
836 793 padding: 0.35rem 0.75rem;
837 794 border: 1px solid var(--blood-dark);
838 795 border-radius: var(--radius);
839 Removed: flex: 1 1 auto;
796 Added: flex: 0 0 auto;
840 797 color: var(--blood-dark);
841 798 background: transparent;
842 799 font-weight: 700;
@@ -854,7 +811,7 @@
854 811 }
855 812 .calendar-grid th {
856 813 padding: 0.25rem 0;
857 Removed: color: var(--muted-ink);
814 Added: color: var(--ink);
858 815 font: 700 var(--font-size-small)/1 var(--sans);
859 816 text-align: center;
860 817 }
lib/web/handlers_logbook.ml
index d242d7fb..70e4cda9 100644..100644
@@ -274,8 +274,8 @@
274 274 in
275 275 let year = query_int "year" (now_tm.Unix.tm_year + 1900) in
276 276 let month = clamp 1 12 (query_int "month" (now_tm.Unix.tm_mon + 1)) in
277 Removed: (* Only the chosen month's entries fill the table. The calendar's year range
278 Removed: still spans all history. *)
277 Added: (* Only the chosen month's entries fill the accordion. The calendar's year
278 Added: range still spans all history. *)
279 279 let in_month (e : View_model.Logbook_entry.t) =
280 280 let tm = Unix.gmtime (float_of_int e.at_unix) in
281 281 tm.Unix.tm_year + 1900 = year && tm.Unix.tm_mon + 1 = month
test/web/logbook.ml
index 1947e4be..f111c3f1 100644..100644
@@ -27,7 +27,7 @@
27 27 "empty state when nothing is logged" true
28 28 (Option.is_some (index_from ~needle:"Nothing logged yet." page 0))
29 29 );
30 Removed: ( "a finished workout appears in the logbook table",
30 Added: ( "a finished workout appears in the logbook accordion",
31 31 `Quick,
32 32 fun () ->
33 33 let c = client () in
@@ -37,19 +37,26 @@
37 37 let token = Option.get (csrf_token (body (get c "/workout"))) in
38 38 let _ = post c "/workout/finish" [ ("dream.csrf", token) ] in
39 39 let page = body (get c "/logbook") in
40 Added: (* The logbook is an accordion, one collapsible <details> per
41 Added: workout. There is no table and no Duration. *)
40 42 Alcotest.(check bool)
41 Removed: "the logbook table is present" true
43 Added: "the accordion is present" true
42 44 (Option.is_some
45 Added: (index_from ~needle:"class=\"logbook-entry\"" page 0));
46 Added: Alcotest.(check bool)
47 Added: "no workout table" false
48 Added: (Option.is_some
43 49 (index_from ~needle:"class=\"logbook-table\"" page 0));
44 Removed: (* Progress moved into the row menu, so there is no Progress column.
45 Removed: Duration remains a column. *)
46 50 Alcotest.(check bool)
47 Removed: "the Duration column header" true
48 Removed: (Option.is_some (index_from ~needle:"Duration" page 0));
51 Added: "the entry summary carries the date" true
52 Added: (Option.is_some (index_from ~needle:"logbook-entry-date" page 0));
49 53 Alcotest.(check bool)
50 Removed: "no Progress column header" false
51 Removed: (Option.is_some (index_from ~needle:">Progress<" page 0)) );
52 Removed: ( "the row menu shows exact Heavy Duty progress evidence",
54 Added: "the entry summary carries the title" true
55 Added: (Option.is_some (index_from ~needle:"logbook-entry-title" page 0));
56 Added: Alcotest.(check bool)
57 Added: "no duration is shown" false
58 Added: (Option.is_some (index_from ~needle:" min<" page 0)) );
59 Added: ( "the accordion panel shows exact Heavy Duty progress evidence",
53 60 `Quick,
54 61 fun () ->
55 62 let c, repo = client_with_repo () in
@@ -92,26 +99,18 @@
92 99 (Lwt_main.run
93 100 (Memory_repo.finish_workout repo trainee.Trainee.id
94 101 (saved ~started:172_800 ~load:12. ~reps:9)));
95 Removed: (* Progress now lives in the row's three-dot menu, always in the
102 Added: (* Progress lives in the entry's accordion panel, always in the
96 103 markup — no expand round-trip. The later workout progressed, so
97 104 its line carries the progress-verdict class and the exact delta. *)
98 105 let page = body (get c "/logbook?year=1970&month=1") in
99 106 List.iter
100 107 (fun needle ->
101 108 Alcotest.(check bool)
102 Removed: ("row menu shows " ^ needle)
109 Added: ("accordion panel shows " ^ needle)
103 110 true
104 111 (Option.is_some (index_from ~needle page 0)))
105 Removed: [
106 Removed: "18 min";
107 Removed: "Laterals";
108 Removed: "12 kg";
109 Removed: "(+2)";
110 Removed: "9 reps";
111 Removed: "(+1)";
112 Removed: "delta-up";
113 Removed: ] );
114 Removed: ( "each saved workout row has an edit and confirmation-gated delete menu",
112 Added: [ "Laterals"; "12 kg"; "(+2)"; "9 reps"; "(+1)"; "delta-up" ] );
113 Added: ( "each saved workout has an edit and confirmation-gated delete action",
115 114 `Quick,
116 115 fun () ->
117 116 let c, repo = client_with_repo () in
@@ -122,13 +121,14 @@
122 121 let token = Option.get (csrf_token (body (get c "/workout"))) in
123 122 let _ = post c "/workout/finish" [ ("dream.csrf", token) ] in
124 123 let page = body (get c "/logbook") in
124 Added: (* Edit and Delete sit inside the entry's accordion panel. *)
125 125 List.iter
126 126 (fun needle ->
127 127 Alcotest.(check bool)
128 Removed: ("row menu contains " ^ needle)
128 Added: ("accordion panel contains " ^ needle)
129 129 true
130 130 (Option.is_some (index_from ~needle page 0)))
131 Removed: [ "⋮"; "Edit"; "Delete"; "/logbook?delete=w1" ];
131 Added: [ "Edit"; "Delete"; "/logbook?delete=w1" ];
132 132 let confirm = body (get c "/logbook?delete=w1") in
133 133 Alcotest.(check bool)
134 134 "delete asks for confirmation" true
@@ -444,7 +444,7 @@
444 444 "only the valid factor survives" true
445 445 (Feedback.signals (List.hd reports)
446 446 = [ Feedback.Difficulty Feedback.Better_than_usual ]) );
447 Removed: ( "a promoted import shows verbatim, read-only, with a three-dot menu",
447 Added: ( "a promoted import shows verbatim, read-only, as an accordion entry",
448 448 `Quick,
449 449 fun () ->
450 450 (* Upload one failure set, map its exercise, promote it, then read
@@ -508,7 +508,7 @@
508 508 "the imported workout title is shown" true
509 509 (Option.is_some (index_from ~needle:"Legs" page 0));
510 510 (* The name is plain -- not italicised -- and there is no caption.
511 Removed: The import is marked by the oxblood pill inside the menu drawer. *)
511 Added: The import is marked by the blood pill in the entry summary. *)
512 512 Alcotest.(check bool)
513 513 "the title is not italicised" false
514 514 (Option.is_some
@@ -518,20 +518,18 @@
518 518 (Option.is_some
519 519 (index_from ~needle:"titles were imported" page 0));
520 520 Alcotest.(check bool)
521 Removed: "the Imported pill is present in the drawer" true
521 Added: "the Imported pill marks the entry" true
522 522 (Option.is_some (index_from ~needle:"logbook-import-pill" page 0));
523 Removed: (* The import spans 10:00-11:00, so its duration is relevant and
524 Removed: shown, like a native workout. *)
523 Added: (* Duration is no longer shown for any workout, native or imported. *)
525 524 Alcotest.(check bool)
526 Removed: "the imported workout duration is shown" true
525 Added: "no duration is shown" false
527 526 (Option.is_some (index_from ~needle:"60 min" page 0));
528 Removed: (* An import's status and detail live in a three-dot menu, whose
529 Removed: drawer lists the imported exercise and its effort. This is a first
530 Removed: performance, so no change is shown. *)
527 Added: (* An import's exercises and efforts live in the accordion panel.
528 Added: This is a first performance, so no change is shown. *)
531 529 Alcotest.(check bool)
532 Removed: "the import three-dot menu drawer is present" true
530 Added: "the entry is an accordion" true
533 531 (Option.is_some
534 Removed: (index_from ~needle:"logbook-import-drawer" page 0));
532 Added: (index_from ~needle:"class=\"logbook-entry\"" page 0));
535 533 Alcotest.(check bool)
536 534 "the imported exercise is listed" true
537 535 (Option.is_some (index_from ~needle:"Squats" page 0));
@@ -768,23 +766,24 @@
768 766 Alcotest.(check bool)
769 767 "the calendar is present" true
770 768 (Option.is_some (index_from ~needle:"class=\"calendar\"" page 0));
769 Added: (* The month and year label is back in the calendar header. *)
771 770 Alcotest.(check bool)
772 Removed: "the calendar shows the month name" true
771 Added: "the calendar header shows the month name" true
773 772 (Option.is_some (index_from ~needle:"January 1970" page 0));
774 773 Alcotest.(check bool)
775 774 "a Today link is present" true
776 775 (Option.is_some (index_from ~needle:"calendar-today-link" page 0));
777 776 Alcotest.(check bool)
778 Removed: "prev/next navigation is present" true
777 Added: "prev/next arrows are present" true
779 778 (Option.is_some (index_from ~needle:"calendar-arrow" page 0));
780 779 Alcotest.(check bool)
781 780 "a workout day is highlighted" true
782 781 (Option.is_some (index_from ~needle:"calendar-workout" page 0));
783 Removed: (* A different month shows only that month: the January workout is
784 Removed: gone from the table, which reads as empty. *)
782 Added: (* Arrow pagination: a different month shows only that month, so the
783 Added: January workout is gone and the month reads as empty. *)
785 784 let other = body (get c "/logbook?year=1970&month=3") in
786 785 Alcotest.(check bool)
787 Removed: "another month's table is empty" true
786 Added: "another month is empty" true
788 787 (Option.is_some
789 788 (index_from ~needle:"Nothing logged yet." other 0));
790 789 Alcotest.(check bool)