feat Simplify responsive profile navigation

Center the mobile brand and move profile access to the logbook. Keep the desktop navigation focused on the primary workout views.

Commit
cc43b73d0071637d867a61274fc84126b46a6b27
Author
Marius Peter <dev@marius-peter.com>
Author date
Committer
Marius Peter <dev@marius-peter.com>
Committer date
Changed files
lib/presenter/page_shell.ml
index b6f21665..f8b3ace2 100644..100644
@@ -2,7 +2,7 @@
2 2 open Page_kit
3 3
4 4 (* The shell. [viewer] and [request] are present on authenticated pages, which
5 Removed: then show a logout control and the username on the history link. Auth pages
5 Added: then show the profile link and the username on the history link. Auth pages
6 6 omit both. *)
7 7 let theme_session_key = "hito.theme"
8 8
@@ -93,74 +93,10 @@
93 93 nav_link "logbook" Routes.logbook "Logbook";
94 94 ]
95 95 in
96 Removed: (* The profile control names the signed-in trainee and opens a drop-down of
97 Removed: account actions: Profile, Settings, and Sign out. It sits in the masthead,
98 Removed: and on mobile it is the top-bar profile control the bottom nav has no room
99 Removed: for. Settings points at the profile page too — account settings live there
100 Removed: and there is no separate settings page. A <details> menu needs no client
101 Removed: script, so it works before the SPA enhancement loads. The Sign-out form is
102 Removed: nested inside and keeps its CSRF token. *)
103 Removed: let profile_area =
104 Removed: match (viewer, request) with
105 Removed: | Some (viewer : View_model.Viewer.t), Some request ->
106 Removed: let menu_link page path label =
107 Removed: let attrs = [ class_ "profile-menu-item"; href path ] in
108 Removed: let attrs = Dream_html.string_attr "role" "menuitem" :: attrs in
109 Removed: let attrs =
110 Removed: if spa_client then Dream_html.attr "data-hito-app-link" :: attrs
111 Removed: else attrs
112 Removed: in
113 Removed: let attrs =
114 Removed: if String.equal page active then
115 Removed: Dream_html.string_attr "aria-current" "page" :: attrs
116 Removed: else attrs
117 Removed: in
118 Removed: tag "a" attrs [ txt "%s" label ]
119 Removed: in
120 Removed: let summary_attrs =
121 Removed: if String.equal "profile" active || String.equal "settings" active
122 Removed: then
123 Removed: [ class_ "profile"; Dream_html.string_attr "aria-current" "page" ]
124 Removed: else [ class_ "profile" ]
125 Removed: in
126 Removed: [
127 Removed: tag "details"
128 Removed: [ class_ "profile-menu" ]
129 Removed: [
130 Removed: tag "summary" summary_attrs
131 Removed: [ txt "%s" viewer.View_model.Viewer.username ];
132 Removed: tag "div"
133 Removed: [
134 Removed: class_ "profile-menu-drawer";
135 Removed: Dream_html.string_attr "role" "menu";
136 Removed: Dream_html.string_attr "aria-label" "Profile menu";
137 Removed: ]
138 Removed: [
139 Removed: menu_link "profile" Routes.profile "Profile";
140 Removed: menu_link "settings" Routes.settings "Settings";
141 Removed: menu_link "app-feedback" Routes.app_feedback "Feedback";
142 Removed: tag "form"
143 Removed: [
144 Removed: action Routes.logout;
145 Removed: post_form;
146 Removed: class_ "logout";
147 Removed: Dream_html.attr "data-hito-app-form";
148 Removed: ]
149 Removed: [
150 Removed: Dream_html.csrf_tag request;
151 Removed: tag "button"
152 Removed: [
153 Removed: type_ "submit";
154 Removed: class_ "profile-menu-item";
155 Removed: Dream_html.string_attr "role" "menuitem";
156 Removed: ]
157 Removed: [ txt "Sign out" ];
158 Removed: ];
159 Removed: ];
160 Removed: ];
161 Removed: ]
162 Removed: | _ -> []
163 Removed: in
96 Added: (* Profile access lives in the logbook action bar. The masthead contains only
97 Added: the three primary navigation links on desktop, and only the brand on
98 Added: mobile. *)
99 Added: let profile_area = [] in
164 100 let primary_nav =
165 101 match (viewer, request) with
166 102 | Some (_ : View_model.Viewer.t), Some _ ->
lib/presenter/pages_logbook.ml
index f04ffd0b..5b34b432 100644..100644
@@ -394,6 +394,13 @@
394 394 [
395 395 tag "a"
396 396 [
397 Added: href Routes.profile;
398 Added: class_ "button";
399 Added: Dream_html.attr "data-hito-app-link";
400 Added: ]
401 Added: [ txt "Profile" ];
402 Added: tag "a"
403 Added: [
397 404 href Routes.progress;
398 405 class_ "button";
399 406 Dream_html.attr "data-hito-app-link";
lib/presenter/pages_profile.ml
index db1bf165..17a446a2 100644..100644
@@ -249,4 +249,46 @@
249 249 void "input" [ type_ "submit"; value "Upload import" ];
250 250 ];
251 251 ];
252 Added: tag "section"
253 Added: [ class_ "profile-section profile-link-section" ]
254 Added: [
255 Added: tag "h2" [] [ txt "Settings" ];
256 Added: tag "p" [] [ txt "Manage your theme and other account settings." ];
257 Added: tag "a"
258 Added: [
259 Added: class_ "button";
260 Added: href Routes.settings;
261 Added: Dream_html.attr "data-hito-app-link";
262 Added: ]
263 Added: [ txt "Open settings" ];
264 Added: ];
265 Added: tag "section"
266 Added: [ class_ "profile-section profile-link-section" ]
267 Added: [
268 Added: tag "h2" [] [ txt "Feedback" ];
269 Added: tag "p" [] [ txt "Share feedback about the application." ];
270 Added: tag "a"
271 Added: [
272 Added: class_ "button";
273 Added: href Routes.app_feedback;
274 Added: Dream_html.attr "data-hito-app-link";
275 Added: ]
276 Added: [ txt "Give feedback" ];
277 Added: ];
278 Added: tag "section"
279 Added: [ class_ "profile-section profile-signout-section" ]
280 Added: [
281 Added: tag "h2" [] [ txt "Sign out" ];
282 Added: tag "form"
283 Added: [
284 Added: action Routes.logout;
285 Added: post_form;
286 Added: class_ "profile-signout-form";
287 Added: Dream_html.attr "data-hito-app-form";
288 Added: ]
289 Added: [
290 Added: Dream_html.csrf_tag request;
291 Added: void "input" [ type_ "submit"; value "Sign out" ];
292 Added: ];
293 Added: ];
252 294 ])
lib/web/assets/hito.css
index 482b851f..b1333129 100644..100644
@@ -255,69 +255,7 @@
255 255 font-weight: 800;
256 256 }
257 257
258 Removed: /* The profile drop-down. The <summary> reuses the .profile pill styling. The
259 Removed: panel opens below the pill and holds the account actions. A <details> menu
260 Removed: needs no script, so it works before the SPA enhancement loads. The global
261 Removed: reduced-motion rule removes the open animation. */
262 Removed: .profile-menu {
263 Removed: position: relative;
264 Removed: display: inline-block;
265 Removed: }
266 Removed: .profile-menu > summary {
267 Removed: list-style: none;
268 Removed: cursor: pointer;
269 Removed: }
270 Removed: .profile-menu-drawer {
271 Removed: position: absolute;
272 Removed: right: 0;
273 Removed: z-index: 30;
274 Removed: min-width: 11rem;
275 Removed: margin-top: 0.35rem;
276 Removed: padding: 0.3rem;
277 Removed: display: flex;
278 Removed: flex-direction: column;
279 Removed: gap: 0.15rem;
280 Removed: background: var(--paper-raised);
281 Removed: border: 1px solid var(--rule-strong);
282 Removed: border-radius: var(--radius);
283 Removed: box-shadow: 0 0.5rem 1.25rem rgba(0, 0, 0, 0.18);
284 Removed: }
285 Removed: .profile-menu[open] > .profile-menu-drawer {
286 Removed: animation: profile-menu-open 150ms ease both;
287 Removed: }
288 Removed: @keyframes profile-menu-open {
289 Removed: from { opacity: 0; transform: translateY(-0.3rem); }
290 Removed: to { opacity: 1; transform: translateY(0); }
291 Removed: }
292 Removed: .profile-menu-item {
293 Removed: display: block;
294 Removed: width: 100%;
295 Removed: min-height: 2.5rem;
296 Removed: box-sizing: border-box;
297 Removed: margin: 0;
298 Removed: padding: 0.5rem 0.7rem;
299 Removed: border: none;
300 Removed: border-radius: var(--radius);
301 Removed: background: transparent;
302 Removed: color: var(--ink);
303 Removed: font: inherit;
304 Removed: font-weight: 700;
305 Removed: text-align: left;
306 Removed: text-decoration: none;
307 Removed: cursor: pointer;
308 Removed: transition: background-color 150ms ease, color 150ms ease;
309 Removed: }
310 Removed: .profile-menu-item:hover {
311 Removed: color: var(--blood-dark);
312 Removed: background: var(--hover-surface);
313 Removed: }
314 Removed: .profile-menu-item[aria-current] {
315 Removed: color: var(--on-primary);
316 Removed: background: var(--blood);
317 Removed: }
318 Removed: .profile-menu .logout {
319 Removed: margin: 0;
320 Removed: }
258 Added: /* The profile link uses the same compact pill as the old account control. */
321 259
322 260 /* The content starts below the masthead. The main element is also the
323 261 replaceable SPA content node, so the page needs no extra surface wrapper. */
@@ -1260,6 +1198,10 @@
1260 1198 padding-top: 1rem;
1261 1199 }
1262 1200 .profile-form { margin-top: 0.5rem; }
1201 Added: .profile-link-section p { margin: 0.5rem 0 0.75rem; }
1202 Added: .profile-link-section .button { display: inline-flex; }
1203 Added: .profile-signout-form { margin-top: 0.75rem; }
1204 Added: .profile-signout-form input[type="submit"] { width: 100%; }
1263 1205
1264 1206 /* A profile value row: the field name, its current value, and the button that
1265 1207 opens the edit modal. */
@@ -1410,6 +1352,11 @@
1410 1352 background: var(--hover-surface);
1411 1353 }
1412 1354 .app-shell { padding: 0 0.75rem calc(6.75rem + env(safe-area-inset-bottom)); }
1355 Added:
1356 Added: @media (max-width: 42.0624rem) {
1357 Added: .masthead { justify-content: center; }
1358 Added: .masthead .brand { margin-right: 0; }
1359 Added: }
1413 1360
1414 1361 @media (min-width: 42.0625rem) {
1415 1362 .app-shell { padding: 0 1rem 2.5rem; }
test/web/profile.ml
index 3a6d4b83..06c5c306 100644..100644
@@ -10,9 +10,28 @@
10 10 fun () ->
11 11 let c = client () in
12 12 let _ = sign_in_new c in
13 Added: let profile_page = body (get c "/profile") in
13 14 Alcotest.(check int)
14 15 "profile served" 200
15 Removed: (status (get c "/profile")) );
16 Added: (status (get c "/profile"));
17 Added: Alcotest.(check bool)
18 Added: "profile has a settings section" true
19 Added: (contains_substring ~needle:"Settings" profile_page);
20 Added: Alcotest.(check bool)
21 Added: "profile has a feedback section" true
22 Added: (contains_substring ~needle:"Feedback" profile_page);
23 Added: Alcotest.(check bool)
24 Added: "profile has a sign-out button" true
25 Added: (contains_substring ~needle:"value=\"Sign out\"" profile_page);
26 Added: let logbook_page = body (get c "/logbook") in
27 Added: Alcotest.(check bool)
28 Added: "logbook has a Profile action" true
29 Added: (contains_substring ~needle:"href=\"/profile\"" logbook_page);
30 Added: Alcotest.(check bool)
31 Added: "masthead has no Profile control" true
32 Added: (not
33 Added: (contains_substring ~needle:"class=\"profile\"" logbook_page))
34 Added: );
16 35 ( "changing the username updates the stored account",
17 36 `Quick,
18 37 fun () ->