View raw

1 open Dream_html 2 open Page_kit 3 4 (* The shell. [viewer] and [request] identify authenticated pages. Auth pages 5 omit the application navigation. *) 6 let theme_session_key = "hito.theme" 7 8 let deployment_version () = 9 match Sys.getenv_opt "HITO_VERSION" with 10 | Some version when version <> "" -> version 11 | _ -> "development" 12 13 let footer () = 14 tag "footer" 15 [ class_ "site-footer" ] 16 [ 17 tag "div" [] [ txt "© 2026 Marius PETER. All rights reserved." ]; 18 tag "div" [] 19 [ 20 txt "Version: "; 21 tag "code" 22 [ class_ "deployment-version" ] 23 [ txt "%s" (deployment_version ()) ]; 24 ]; 25 ] 26 27 let html_page ?viewer ?request ?(active = "") ?(show_nav = false) 28 ?(show_footer = false) ?workout_started title content = 29 let spa_client = true in 30 (* The trainee is logging a workout exactly when its start epoch is present. 31 The middle nav slot then shows the elapsed timer on every page, so the 32 running workout is always visible; otherwise it is the routine link. *) 33 let theme = 34 match request with 35 | Some request -> ( 36 match Dream.session_field request theme_session_key with 37 | Some "dark" -> "dark" 38 | _ -> "light") 39 | None -> "light" 40 in 41 let nav_link page path label = 42 let attrs = [ href path ] in 43 let attrs = 44 if spa_client then Dream_html.attr "data-hito-app-link" :: attrs 45 else attrs 46 in 47 let attrs = 48 if String.equal page active then 49 Dream_html.string_attr "aria-current" "page" :: attrs 50 else attrs 51 in 52 tag "a" attrs [ txt "%s" label ] 53 in 54 (* Three always-available actions. The middle slot points to the routine 55 normally. While logging it becomes the elapsed workout timer. The link 56 still carries the trainee to the current workout, so a running workout 57 stays one tap away without adding a fourth destination. The client ticks 58 the value from [data-started]. Without script the link shows a static 59 start. *) 60 let workout_timer_link started = 61 let attrs = 62 [ href Routes.workout; class_ "nav-timer" ] 63 @ (if spa_client then [ Dream_html.attr "data-hito-app-link" ] else []) 64 @ (if String.equal "workout" active then 65 [ Dream_html.string_attr "aria-current" "page" ] 66 else []) 67 @ [ 68 Dream_html.attr "data-hito-workout-timer"; 69 Dream_html.string_attr "data-started" "%d" started; 70 Dream_html.string_attr "role" "timer"; 71 Dream_html.string_attr "aria-label" "Elapsed workout time"; 72 ] 73 in 74 tag "a" attrs 75 [ 76 tag "span" 77 [ 78 class_ "nav-timer-value"; 79 Dream_html.string_attr "aria-live" "off"; 80 Dream_html.attr "data-hito-workout-timer-value"; 81 ] 82 [ txt "0:00" ]; 83 ] 84 in 85 let navigation_links () = 86 [ 87 nav_link "home" Routes.home "Home"; 88 (match workout_started with 89 | Some started -> workout_timer_link started 90 | None -> nav_link "routine" Routes.routine "Routine"); 91 nav_link "logbook" Routes.logbook "Logbook"; 92 ] 93 in 94 let application_navigation () = 95 let visible = 96 match (viewer, request) with 97 | Some (_ : View_model.Viewer.t), Some _ -> true 98 | _ -> show_nav 99 in 100 if visible then 101 [ 102 tag "nav" 103 [ 104 class_ "app-nav"; 105 Dream_html.string_attr "aria-label" "Application navigation"; 106 ] 107 (navigation_links ()); 108 ] 109 else [] 110 in 111 tag "html" 112 [ 113 Dream_html.string_attr "lang" "en"; 114 Dream_html.string_attr "data-theme" "%s" theme; 115 ] 116 [ 117 tag "head" [] 118 [ 119 tag "title" [] [ txt "Hito — %s" title ]; 120 void "meta" [ Dream_html.string_attr "charset" "utf-8" ]; 121 void "meta" 122 [ 123 Dream_html.string_attr "name" "viewport"; 124 Dream_html.string_attr "content" 125 "width=device-width,initial-scale=1"; 126 ]; 127 void "link" [ rel "stylesheet"; href Routes.stylesheet ]; 128 void "link" 129 [ 130 rel "icon"; 131 Dream_html.string_attr "type" "image/svg+xml"; 132 href Routes.favicon; 133 ]; 134 ]; 135 tag "body" 136 [ Dream_html.string_attr "class" "hito-app" ] 137 ([ 138 tag "a" 139 [ 140 class_ "skip-link"; Dream_html.string_attr "href" "#main-content"; 141 ] 142 [ txt "Skip to main content" ]; 143 tag "div" 144 ([ 145 Dream_html.string_attr "class" "app-shell page-%s" active; 146 Dream_html.string_attr "data-hito-page-title" "Hito — %s" title; 147 ] 148 @ 149 if spa_client then [ Dream_html.attr "data-hito-app-shell" ] 150 else []) 151 ([ 152 tag "header" 153 [ class_ "masthead" ] 154 ([ 155 tag "a" 156 ([ class_ "brand"; href Routes.home ] 157 @ 158 if spa_client then 159 [ Dream_html.attr "data-hito-app-link" ] 160 else []) 161 [ txt "hito" ]; 162 ] 163 @ application_navigation ()); 164 tag "main" 165 ([ id "main-content"; tabindex "-1" ] 166 @ 167 if spa_client then [ Dream_html.attr "data-hito-app-content" ] 168 else []) 169 content; 170 ] 171 @ if show_footer then [ footer () ] else []); 172 ] 173 @ 174 if spa_client then 175 [ 176 tag "script" 177 [ src Routes.workout_client; Dream_html.attr "defer" ] 178 []; 179 ] 180 else []); 181 ] 182