open Dream_html open Page_kit (* The shell. [viewer] and [request] identify authenticated pages. Auth pages omit the application navigation. *) let theme_session_key = "hito.theme" let deployment_version () = match Sys.getenv_opt "HITO_VERSION" with | Some version when version <> "" -> version | _ -> "development" let footer () = tag "footer" [ class_ "site-footer" ] [ tag "div" [] [ txt "© 2026 Marius PETER. All rights reserved." ]; tag "div" [] [ txt "Version: "; tag "code" [ class_ "deployment-version" ] [ txt "%s" (deployment_version ()) ]; ]; ] let html_page ?viewer ?request ?(active = "") ?(show_nav = false) ?(show_footer = false) ?workout_started title content = let spa_client = true in (* The trainee is logging a workout exactly when its start epoch is present. The middle nav slot then shows the elapsed timer on every page, so the running workout is always visible; otherwise it is the routine link. *) let theme = match request with | Some request -> ( match Dream.session_field request theme_session_key with | Some "dark" -> "dark" | _ -> "light") | None -> "light" in let nav_link page path label = let attrs = [ href path ] in let attrs = if spa_client then Dream_html.attr "data-hito-app-link" :: attrs else attrs in let attrs = if String.equal page active then Dream_html.string_attr "aria-current" "page" :: attrs else attrs in tag "a" attrs [ txt "%s" label ] in (* Three always-available actions. The middle slot points to the routine normally. While logging it becomes the elapsed workout timer. The link still carries the trainee to the current workout, so a running workout stays one tap away without adding a fourth destination. The client ticks the value from [data-started]. Without script the link shows a static start. *) let workout_timer_link started = let attrs = [ href Routes.workout; class_ "nav-timer" ] @ (if spa_client then [ Dream_html.attr "data-hito-app-link" ] else []) @ (if String.equal "workout" active then [ Dream_html.string_attr "aria-current" "page" ] else []) @ [ Dream_html.attr "data-hito-workout-timer"; Dream_html.string_attr "data-started" "%d" started; Dream_html.string_attr "role" "timer"; Dream_html.string_attr "aria-label" "Elapsed workout time"; ] in tag "a" attrs [ tag "span" [ class_ "nav-timer-value"; Dream_html.string_attr "aria-live" "off"; Dream_html.attr "data-hito-workout-timer-value"; ] [ txt "0:00" ]; ] in let navigation_links () = [ nav_link "home" Routes.home "Home"; (match workout_started with | Some started -> workout_timer_link started | None -> nav_link "routine" Routes.routine "Routine"); nav_link "logbook" Routes.logbook "Logbook"; ] in let application_navigation () = let visible = match (viewer, request) with | Some (_ : View_model.Viewer.t), Some _ -> true | _ -> show_nav in if visible then [ tag "nav" [ class_ "app-nav"; Dream_html.string_attr "aria-label" "Application navigation"; ] (navigation_links ()); ] else [] in tag "html" [ Dream_html.string_attr "lang" "en"; Dream_html.string_attr "data-theme" "%s" theme; ] [ tag "head" [] [ tag "title" [] [ txt "Hito — %s" title ]; void "meta" [ Dream_html.string_attr "charset" "utf-8" ]; void "meta" [ Dream_html.string_attr "name" "viewport"; Dream_html.string_attr "content" "width=device-width,initial-scale=1"; ]; void "link" [ rel "stylesheet"; href Routes.stylesheet ]; void "link" [ rel "icon"; Dream_html.string_attr "type" "image/svg+xml"; href Routes.favicon; ]; ]; tag "body" [ Dream_html.string_attr "class" "hito-app" ] ([ tag "a" [ class_ "skip-link"; Dream_html.string_attr "href" "#main-content"; ] [ txt "Skip to main content" ]; tag "div" ([ Dream_html.string_attr "class" "app-shell page-%s" active; Dream_html.string_attr "data-hito-page-title" "Hito — %s" title; ] @ if spa_client then [ Dream_html.attr "data-hito-app-shell" ] else []) ([ tag "header" [ class_ "masthead" ] ([ tag "a" ([ class_ "brand"; href Routes.home ] @ if spa_client then [ Dream_html.attr "data-hito-app-link" ] else []) [ txt "hito" ]; ] @ application_navigation ()); tag "main" ([ id "main-content"; tabindex "-1" ] @ if spa_client then [ Dream_html.attr "data-hito-app-content" ] else []) content; ] @ if show_footer then [ footer () ] else []); ] @ if spa_client then [ tag "script" [ src Routes.workout_client; Dream_html.attr "defer" ] []; ] else []); ]