[OCaml] High Intensity Training Online
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.
Changed files
lib/presenter/page_shell.ml
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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 () ->