type page = Dream_html.node let tag = Dream_html.std_tag let void = Dream_html.void_tag let class_ value = Dream_html.string_attr "class" value let name value = Dream_html.string_attr "name" value let value value = Dream_html.string_attr "value" value let rel value = Dream_html.string_attr "rel" value let type_ value = Dream_html.string_attr "type" value let step value = Dream_html.string_attr "step" value let id value = Dream_html.string_attr "id" value let tabindex value = Dream_html.string_attr "tabindex" value let required = Dream_html.attr "required" let href path = Dream_html.path_attr (Dream_html.uri_attr "href") path let src path = Dream_html.path_attr (Dream_html.uri_attr "src") path let action path = Dream_html.path_attr (Dream_html.uri_attr "action") path let post_form = Dream_html.string_attr "method" "post" let alert message = tag "p" [ class_ "warn"; Dream_html.string_attr "role" "alert" ] [ Dream_html.txt "%s" message ] (* Render server-side link tabs. The active tab uses [aria-current] because each tab navigates to a complete page rather than switching a client panel. *) let link_tabs ~aria_label ~active tabs = let tab_link (key, label, path) = let attrs = [ class_ "page-tab"; Dream_html.string_attr "href" "%s" path; Dream_html.attr "data-hito-app-link"; ] in let attrs = if String.equal key active then Dream_html.string_attr "aria-current" "page" :: attrs else attrs in tag "a" attrs [ Dream_html.txt "%s" label ] in tag "nav" [ class_ "page-tabs"; Dream_html.string_attr "aria-label" "%s" aria_label ] (List.map tab_link tabs) (* Render the common comparison table structure. The caller supplies grouped bodies, so recorded and planned workouts keep their different values. *) let workout_evidence_table ?(upcoming = false) ?(caption_class = "sr-only") ~caption bodies = let classes = if upcoming then "workout-evidence-table upcoming-workout-table" else "workout-evidence-table" in let column label = tag "th" [ Dream_html.string_attr "scope" "col" ] [ Dream_html.txt "%s" label ] in let headers = [ column "Exercise"; column "Load (kg)" ] @ if upcoming then [ column "Reps" ] else [ column "Δ"; column "Reps"; column "Δ" ] in tag "table" [ class_ "%s" classes ] ([ tag "caption" [ class_ "%s" caption_class ] caption; tag "thead" [] [ tag "tr" [] headers ]; ] @ bodies) (* Render a prescribed workout before it has a log. Both Home and Logbook use this component, so the same upcoming workout shows the same evidence. *) let upcoming_workout_card (planned : View_model.Upcoming_workout.t) = let format_load load = Printf.sprintf "%g" load in let format_reps reps = string_of_int reps in let rows_of_group (group : View_model.Upcoming_workout.group) = List.map (fun (exercise : View_model.Upcoming_workout.exercise) -> tag "tr" [] [ tag "th" [ Dream_html.string_attr "scope" "row"; class_ "evidence-exercise"; ] [ Dream_html.txt "%s" exercise.name ]; tag "td" [ class_ "observation-latest" ] [ Dream_html.txt "%s" (Option.fold ~none:"—" ~some:format_load exercise.latest_load); ]; tag "td" [ class_ "observation-latest" ] [ Dream_html.txt "%s" (Option.fold ~none:"—" ~some:format_reps exercise.latest_reps); ]; ]) group.exercises in let caption = [ tag "strong" [ class_ "workout-evidence-caption-name" ] [ Dream_html.txt "%s, %s" planned.routine_name planned.name ]; tag "span" [ class_ "workout-evidence-caption-status upcoming" ] [ Dream_html.txt "Planned on %s" planned.scheduled_on ]; ] in tag "article" [ class_ "selected-upcoming-workout" ] [ workout_evidence_table ~upcoming:true ~caption_class:"upcoming-workout-caption" ~caption (List.map (fun group -> tag "tbody" [] (rows_of_group group)) planned.groups); ]