refactor remove stale markup, styles, and client code

A final review before checkout found code that no current page uses. This commit removes it and simplifies what remains. - The client no longer handles data-hito-workout-link or data-hito-feedback-open. No page emits these attributes. The workout-only swap path served only the first attribute. - The stylesheet loses a declaration block without a selector. It was a leftover of the removed planned exercise list. - Pages no longer emit attributes and classes that no style, script, or test reads. The workout page loses its outer wrapper. - The begin forms share their hidden fields. The extension amount shares its reps or seconds label. - The current navigation link gets its fill from aria-current only. The reset button uses the shared button skin, without !important. - Motion durations and shadows become custom properties. - One delegated click listener replaces four. - An unreadable workout start keeps the server value. The timer no longer counts from the epoch. The slot panel keeps its inner body: a padded panel cannot collapse to zero height. The dialog backdrop keeps a literal colour, because older browsers do not let ::backdrop read custom properties. No training rule changes.

Commit
eee8c115a9b18cb75037fa481805067c00570bda
Author
Marius Peter <dev@marius-peter.com>
Author date
Committer
Marius Peter <dev@marius-peter.com>
Committer date
Changed files
lib/presenter/page_kit.ml
index 0fd607cb..622e6373 100644..100644
@@ -15,7 +15,6 @@
15 15 let src path = Dream_html.path_attr (Dream_html.uri_attr "src") path
16 16 let action path = Dream_html.path_attr (Dream_html.uri_attr "action") path
17 17 let post_form = Dream_html.string_attr "method" "post"
18 Removed: let flash_key = "hito.flash"
19 18
20 19 let alert message =
21 20 tag "p"
@@ -103,9 +102,9 @@
103 102 in
104 103 let caption =
105 104 [
106 Removed: tag "span"
105 Added: tag "strong"
107 106 [ class_ "workout-evidence-caption-name" ]
108 Removed: [ tag "strong" [] [ Dream_html.txt "%s" planned.name ] ];
107 Added: [ Dream_html.txt "%s" planned.name ];
109 108 tag "span"
110 109 [ class_ "workout-evidence-caption-status upcoming" ]
111 110 [ Dream_html.txt "Planned on %s" planned.scheduled_on ];
lib/presenter/pages_logbook.ml
index 23a2eafd..ba253f4e 100644..100644
@@ -174,9 +174,9 @@
174 174 let workout_evidence_table (entry : View_model.Recorded_workout.t) groups =
175 175 let caption =
176 176 [
177 Removed: tag "span"
177 Added: tag "strong"
178 178 [ class_ "workout-evidence-caption-name" ]
179 Removed: [ tag "strong" [] [ txt "%s" entry.workout_name ] ];
179 Added: [ txt "%s" entry.workout_name ];
180 180 tag "span"
181 181 [ class_ "workout-evidence-caption-status" ]
182 182 [ txt "Performed on %s" entry.performed_on ];
@@ -186,9 +186,7 @@
186 186 ~caption
187 187 (List.map
188 188 (fun (group : View_model.Stimulus_group.t) ->
189 Removed: tag "tbody"
190 Removed: [ class_ "logbook-stimulus-group" ]
191 Removed: (List.concat_map comparison_rows group.rows))
189 Added: tag "tbody" [] (List.concat_map comparison_rows group.rows))
192 190 groups)
193 191
194 192 let calendar_month_name month =
@@ -238,10 +236,7 @@
238 236
239 237 let selected_workout_card (entry : View_model.Recorded_workout.t) =
240 238 let performance =
241 Removed: if entry.groups = [] then
242 Removed: [
243 Removed: tag "p" [ class_ "logbook-no-progress" ] [ txt "No exercises recorded" ];
244 Removed: ]
239 Added: if entry.groups = [] then [ tag "p" [] [ txt "No exercises recorded" ] ]
245 240 else [ workout_evidence_table entry entry.groups ]
246 241 in
247 242 let actions =
lib/presenter/pages_profile.ml
index 0cff07eb..63c75c03 100644..100644
@@ -29,7 +29,6 @@
29 29 tag "div"
30 30 [ class_ "profile-value" ]
31 31 [
32 Removed: tag "span" [ class_ "profile-value-label" ] [ txt "Username" ];
33 32 tag "span"
34 33 [ class_ "profile-value-text" ]
35 34 [ txt "%s" viewer.View_model.Viewer.username ];
@@ -102,7 +101,6 @@
102 101 tag "div"
103 102 [ class_ "profile-value" ]
104 103 [
105 Removed: tag "span" [ class_ "profile-value-label" ] [ txt "Password" ];
106 104 tag "span" [ class_ "profile-value-text" ] [ txt "••••••••" ];
107 105 tag "button"
108 106 [
lib/presenter/pages_routine.ml
index 34447fe6..9cf8c592 100644..100644
@@ -57,25 +57,29 @@
57 57 routines
58 58 @ home_utility_sections viewer)
59 59
60 Added: (* Both begin forms post the same fields. Only the override flag differs. *)
61 Added: let begin_fields request ~routine_id ~override =
62 Added: [
63 Added: Dream_html.csrf_tag request;
64 Added: void "input"
65 Added: [
66 Added: type_ "hidden";
67 Added: name "routine";
68 Added: Dream_html.string_attr "value" "%s" routine_id;
69 Added: ];
70 Added: void "input"
71 Added: [
72 Added: type_ "hidden";
73 Added: name "override";
74 Added: Dream_html.string_attr "value" "%s" (string_of_bool override);
75 Added: ];
76 Added: ]
77 Added:
60 78 let begin_form request ~routine_id ~override label =
61 79 tag "form"
62 80 [ action Routes.workout; post_form; Dream_html.attr "data-hito-app-form" ]
63 Removed: [
64 Removed: Dream_html.csrf_tag request;
65 Removed: void "input"
66 Removed: [
67 Removed: type_ "hidden";
68 Removed: name "routine";
69 Removed: Dream_html.string_attr "value" "%s" routine_id;
70 Removed: ];
71 Removed: void "input"
72 Removed: [
73 Removed: type_ "hidden";
74 Removed: name "override";
75 Removed: Dream_html.string_attr "value" "%s" (string_of_bool override);
76 Removed: ];
77 Removed: void "input" [ type_ "submit"; value label ];
78 Removed: ]
81 Added: (begin_fields request ~routine_id ~override
82 Added: @ [ void "input" [ type_ "submit"; value label ] ])
79 83
80 84 (* Beginning under override needs an explicit acknowledgement, so its submit is
81 85 gated behind a confirmation modal. Without script the form submits at once —
@@ -91,28 +95,16 @@
91 95 Dream_html.attr "data-hito-app-form";
92 96 Dream_html.attr "data-hito-confirm-form";
93 97 ]
94 Removed: [
95 Removed: Dream_html.csrf_tag request;
96 Removed: void "input"
97 Removed: [
98 Removed: type_ "hidden";
99 Removed: name "routine";
100 Removed: Dream_html.string_attr "value" "%s" routine_id;
101 Removed: ];
102 Removed: void "input"
103 Removed: [
104 Removed: type_ "hidden";
105 Removed: name "override";
106 Removed: Dream_html.string_attr "value" "true";
107 Removed: ];
108 Removed: void "input"
109 Removed: [
110 Removed: type_ "submit";
111 Removed: class_ "outline";
112 Removed: value "Begin under override";
113 Removed: Dream_html.attr "data-hito-confirm-cancel";
114 Removed: ];
115 Removed: ];
98 Added: (begin_fields request ~routine_id ~override:true
99 Added: @ [
100 Added: void "input"
101 Added: [
102 Added: type_ "submit";
103 Added: class_ "outline";
104 Added: value "Begin under override";
105 Added: Dream_html.attr "data-hito-confirm-cancel";
106 Added: ];
107 Added: ]);
116 108 tag "dialog"
117 109 [ class_ "confirm-dialog"; Dream_html.attr "data-hito-confirm-modal" ]
118 110 [
lib/presenter/pages_workout.ml
index d71ed26a..70ddbf94 100644..100644
@@ -20,6 +20,15 @@
20 20 option "static" "then a static hold";
21 21 ]
22 22
23 Added: (* The extension amount counts reps or seconds. Both words are rendered, and
24 Added: the CSS shows the one that matches the selected ending. *)
25 Added: let reps_or_seconds ~reps ~seconds =
26 Added: [
27 Added: tag "span" [ class_ "amount-label-reps" ] [ txt "%s" reps ];
28 Added: tag "span" [ class_ "amount-label-sep" ] [ txt " / " ];
29 Added: tag "span" [ class_ "amount-label-seconds" ] [ txt "%s" seconds ];
30 Added: ]
31 Added:
23 32 (* One movement is one item in the prescribed sequence. Its name, load, and
24 33 reps stay together, so a responsive layout cannot mix two movements. *)
25 34 let movement_group ~name ~load_id ~load_field ~reps_id ~reps_field ?load_value
@@ -128,15 +137,8 @@
128 137 (field_id "extension_amount");
129 138 class_ "sr-only";
130 139 ]
131 Removed: [
132 Removed: tag "span"
133 Removed: [ class_ "amount-label-reps" ]
134 Removed: [ txt "Extension reps" ];
135 Removed: tag "span" [ class_ "amount-label-sep" ] [ txt " / " ];
136 Removed: tag "span"
137 Removed: [ class_ "amount-label-seconds" ]
138 Removed: [ txt "Hold seconds" ];
139 Removed: ];
140 Added: (reps_or_seconds ~reps:"Extension reps"
141 Added: ~seconds:"Hold seconds");
140 142 tag "div"
141 143 [ class_ "input-group" ]
142 144 [
@@ -153,13 +155,7 @@
153 155 ];
154 156 tag "span"
155 157 [ class_ "input-unit" ]
156 Removed: [
157 Removed: tag "span" [ class_ "amount-label-reps" ] [ txt "reps" ];
158 Removed: tag "span" [ class_ "amount-label-sep" ] [ txt " / " ];
159 Removed: tag "span"
160 Removed: [ class_ "amount-label-seconds" ]
161 Removed: [ txt "seconds" ];
162 Removed: ];
158 Added: (reps_or_seconds ~reps:"reps" ~seconds:"seconds");
163 159 ];
164 160 ];
165 161 ];
@@ -176,13 +172,7 @@
176 172 Dream_html.string_attr "value" "%s" submit;
177 173 Dream_html.attr "data-hito-correction-action";
178 174 ];
179 Removed: tag "button"
180 Removed: [
181 Removed: type_ "reset";
182 Removed: class_ "secondary";
183 Removed: Dream_html.attr "data-hito-correction-reset";
184 Removed: ]
185 Removed: [ txt "Reset" ];
175 Added: tag "button" [ type_ "reset"; class_ "secondary" ] [ txt "Reset" ];
186 176 ]
187 177 else
188 178 void "input"
@@ -328,7 +318,6 @@
328 318 action Routes.cancel_workout;
329 319 post_form;
330 320 Dream_html.attr "data-hito-confirm-form";
331 Removed: Dream_html.attr "data-hito-cancel-form";
332 321 ]
333 322 [
334 323 Dream_html.csrf_tag request;
@@ -371,28 +360,19 @@
371 360 ]
372 361 in
373 362 html_page ~viewer ~request ~active:"workout" ?workout_started vm.name
374 Removed: [
375 Removed: tag "div"
376 Removed: (if enhanced then
377 Removed: [
378 Removed: Dream_html.attr "data-hito-workout";
379 Removed: Dream_html.attr "data-hito-workout-content";
380 Removed: ]
381 Removed: else [])
382 Removed: ([
383 Removed: tag "h1" [] [ txt "%s" vm.name ];
384 Removed: tag "h2" [ class_ "workout-date" ] [ txt "%s" vm.date ];
385 Removed: ]
386 Removed: @ slots @ complete_note @ finish_section
387 Removed: @
388 Removed: if enhanced then
363 Added: ([
364 Added: tag "h1" [] [ txt "%s" vm.name ];
365 Added: tag "h2" [ class_ "workout-date" ] [ txt "%s" vm.date ];
366 Added: ]
367 Added: @ slots @ complete_note @ finish_section
368 Added: @
369 Added: if enhanced then
370 Added: [
371 Added: tag "p"
389 372 [
390 Removed: tag "p"
391 Removed: [
392 Removed: Dream_html.string_attr "aria-live" "polite";
393 Removed: Dream_html.attr "data-hito-workout-status";
394 Removed: ]
395 Removed: [];
373 Added: Dream_html.string_attr "aria-live" "polite";
374 Added: Dream_html.attr "data-hito-workout-status";
396 375 ]
397 Removed: else []);
398 Removed: ]
376 Added: [];
377 Added: ]
378 Added: else [])
lib/web/assets/hito.css
index 88bc6179..4e7d7c4f 100644..100644
@@ -66,6 +66,16 @@
66 66 --font-size-h2: 1.777rem;
67 67 --font-size-h1: 2.369rem;
68 68 --radius: 0.4em;
69 Added: /* Motion durations. Fast is a button response, modal is a dialog entry, and
70 Added: base is a page or accordion change. */
71 Added: --duration-fast: 150ms;
72 Added: --duration-modal: 180ms;
73 Added: --duration-base: 220ms;
74 Added: /* Elevation. Every shadow uses the same warm near-black tint. The dialog
75 Added: backdrop keeps a literal value, because older browsers do not let
76 Added: ::backdrop inherit custom properties. */
77 Added: --shadow-modal: 0 0.6rem 2rem rgba(26, 26, 22, 0.25);
78 Added: --shadow-nav: 0 -0.35rem 1.2rem rgba(26, 26, 22, 0.15);
69 79 --sans: "Work Sans", ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
70 80 --mono: "Courier Prime", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
71 81 }
@@ -188,15 +198,12 @@
188 198 .eyebrow { color: var(--blood-dark); }
189 199
190 200
191 Removed: .page-home .app-nav > a[href="/"],
192 Removed: .page-routine .app-nav > a[href="/routine"],
193 Removed: .page-workout .app-nav > a[href="/workout"],
194 Removed: .page-logbook .app-nav > a[href="/logbook"] {
201 Added: /* The current page link. The hover selector keeps the fill when the pointer
202 Added: rests on it, because the plain hover rule comes later in the sheet. */
203 Added: .app-nav > a[aria-current],
204 Added: .app-nav > a[aria-current]:hover {
195 205 color: var(--on-primary);
196 206 background: var(--blood);
197 Removed: }
198 Removed:
199 Removed: .app-nav > a[aria-current] {
200 207 font-weight: 800;
201 208 }
202 209
@@ -216,9 +223,8 @@
216 223 from { opacity: 0; transform: translateY(0.5rem); }
217 224 to { opacity: 1; transform: translateY(0); }
218 225 }
219 Removed: [data-hito-app-content],
220 Removed: [data-hito-workout-content] {
221 Removed: animation: page-enter 220ms ease both;
226 Added: [data-hito-app-content] {
227 Added: animation: page-enter var(--duration-base) ease both;
222 228 }
223 229
224 230 /* One coherent heading scale across the app. h1 is the page title, h2 a
@@ -414,7 +420,8 @@
414 420 a.button,
415 421 .button-group button,
416 422 .button-group input[type="submit"],
417 Removed: .confirm-dialog button {
423 Added: .confirm-dialog button,
424 Added: button[type="reset"].secondary {
418 425 min-height: 3rem;
419 426 border: 2px solid var(--blood-dark);
420 427 border-radius: var(--radius);
@@ -422,13 +429,16 @@
422 429 background: var(--blood);
423 430 cursor: pointer;
424 431 font: 700 var(--font-size-small)/1 var(--sans);
425 Removed: transition: background-color 150ms ease, transform 150ms ease;
432 Added: transition:
433 Added: background-color var(--duration-fast) ease,
434 Added: transform var(--duration-fast) ease;
426 435 }
427 436 input[type="submit"]:hover,
428 437 a.button:hover,
429 438 .button-group button:hover,
430 439 .button-group input[type="submit"]:hover,
431 Removed: .confirm-dialog button:hover {
440 Added: .confirm-dialog button:hover,
441 Added: button[type="reset"].secondary:hover {
432 442 background: var(--blood-hover);
433 443 transform: translateY(-2px);
434 444 }
@@ -488,21 +498,6 @@
488 498 color: var(--on-brass);
489 499 background: var(--brass);
490 500 }
491 Removed: /* A reset button styled to match a secondary submit: it carries the shared
492 Removed: skin's border, radius, font, and transition, which the solid selector group
493 Removed: does not give it, plus the brass outline above and a hover lift. */
494 Removed: button[type="reset"].secondary {
495 Removed: min-height: 3rem;
496 Removed: margin-top: 1rem;
497 Removed: border: 2px solid var(--brass);
498 Removed: border-radius: var(--radius);
499 Removed: cursor: pointer;
500 Removed: font: 700 var(--font-size-small)/1 var(--sans);
501 Removed: transition: background-color 150ms ease, transform 150ms ease;
502 Removed: }
503 Removed: button[type="reset"].secondary:hover {
504 Removed: transform: translateY(-2px);
505 Removed: }
506 501
507 502 /* Blood outline: a blood border and blood text on a transparent field
508 503 that fills blood on hover. Shared by the anchor outline variant and the
@@ -578,9 +573,6 @@
578 573 .workout-evidence-caption-status {
579 574 display: block;
580 575 }
581 Removed: .workout-evidence-caption-name {
582 Removed: font-weight: 700;
583 Removed: }
584 576 .workout-evidence-caption-status.upcoming {
585 577 font-style: italic;
586 578 }
@@ -650,10 +642,6 @@
650 642 color: var(--ink);
651 643 font-family: var(--mono);
652 644 }
653 Removed: margin: 0.65rem 0 0;
654 Removed: color: var(--muted-ink);
655 Removed: font-size: var(--font-size-small);
656 Removed: }
657 645
658 646 .logbook-import-pill {
659 647 display: inline-block;
@@ -1019,7 +1007,7 @@
1019 1007 }
1020 1008 /* The set ending is a property of the completed stimulus. Its spacing and
1021 1009 legend make that distinction clear without a second card. */
1022 Removed: .record-action { margin: 0 !important; }
1010 Added: input[type="submit"].record-action { margin: 0; }
1023 1011 .correction-actions {
1024 1012 display: grid;
1025 1013 grid-template-columns: minmax(0, 2fr) minmax(0, 1fr);
@@ -1165,13 +1153,15 @@
1165 1153 grid-template-rows: 0fr;
1166 1154 visibility: hidden;
1167 1155 transition:
1168 Removed: grid-template-rows 220ms ease,
1169 Removed: visibility 220ms;
1156 Added: grid-template-rows var(--duration-base) ease,
1157 Added: visibility var(--duration-base);
1170 1158 }
1171 1159 .slot-accordion[open]::details-content {
1172 1160 grid-template-rows: 1fr;
1173 1161 visibility: visible;
1174 1162 }
1163 Added: /* The panel clips the content while the grid row animates. The padding must
1164 Added: sit on an inner body, because a padded panel cannot shrink to zero height. */
1175 1165 .slot-accordion > .slot-panel {
1176 1166 min-height: 0;
1177 1167 overflow: clip;
@@ -1211,13 +1201,13 @@
1211 1201 border-top: 6px solid var(--blood);
1212 1202 border-radius: var(--radius);
1213 1203 background: var(--paper-raised);
1214 Removed: box-shadow: 0 0.6rem 2rem rgba(26, 26, 22, 0.25);
1204 Added: box-shadow: var(--shadow-modal);
1215 1205 padding: clamp(1.25rem, 3vw, 1.75rem);
1216 1206 color: var(--ink);
1217 1207 }
1218 1208 .confirm-dialog[open],
1219 1209 .feedback-modal[open],
1220 Removed: .profile-dialog[open] { animation: modal-enter 180ms ease both; }
1210 Added: .profile-dialog[open] { animation: modal-enter var(--duration-modal) ease both; }
1221 1211 .confirm-dialog::backdrop,
1222 1212 .feedback-modal::backdrop,
1223 1213 .profile-dialog::backdrop { background: rgba(26, 26, 22, 0.45); }
@@ -1284,7 +1274,6 @@
1284 1274 gap: 0.5rem 1rem;
1285 1275 margin-top: 0.5rem;
1286 1276 }
1287 Removed: .profile-value-label { color: var(--muted-ink); }
1288 1277 .profile-value-text {
1289 1278 font: 600 var(--font-size-lead)/1.2 var(--sans);
1290 1279 margin-right: auto;
@@ -1366,6 +1355,8 @@
1366 1355 }
1367 1356 }
1368 1357
1358 Added: /* On the narrowest phones, two buttons side by side truncate their labels.
1359 Added: Stack each button group below 26rem. */
1369 1360 @media (max-width: 26rem) {
1370 1361 .button-group { grid-template-columns: 1fr; }
1371 1362 }
@@ -1411,7 +1402,7 @@
1411 1402 gap: 0.4rem;
1412 1403 border-top: 1px solid var(--rule);
1413 1404 background: var(--paper);
1414 Removed: box-shadow: 0 -0.35rem 1.2rem rgba(26, 26, 22, 0.15);
1405 Added: box-shadow: var(--shadow-nav);
1415 1406 padding: 0.5rem 0.6rem calc(0.5rem + env(safe-area-inset-bottom));
1416 1407 }
1417 1408 .app-nav > a {
lib/web/workout_client.ml
index 4f1eb336..50ea902e 100644..100644
@@ -1,7 +1,6 @@
1 1 open Js_of_ocaml
2 2 open Lwt.Infix
3 3
4 Removed: let workout_selector = "[data-hito-workout-content]"
5 4 let app_selector = "[data-hito-app-shell]"
6 5 let document = Dom_html.document
7 6 let request_number = ref 0
@@ -56,14 +55,15 @@
56 55 start and the current time are kept as floats. Only the small
57 56 elapsed difference is narrowed to an [int]. If you subtract in
58 57 [int], the value overflows and yields a wrong count. *)
59 Removed: let started =
60 Removed: try float_of_string (String.trim (Js.to_string started))
61 Removed: with _ -> 0.
62 Removed: in
63 Removed: let elapsed = int_of_float (now -. started) in
64 Removed: match query_one timer "[data-hito-workout-timer-value]" with
65 Removed: | None -> ()
66 Removed: | Some value ->
58 Added: (* An unreadable start keeps the server-rendered value. A fallback
59 Added: to zero would show the time since the epoch. *)
60 Added: match
61 Added: ( float_of_string_opt (String.trim (Js.to_string started)),
62 Added: query_one timer "[data-hito-workout-timer-value]" )
63 Added: with
64 Added: | None, _ | _, None -> ()
65 Added: | Some started, Some value ->
66 Added: let elapsed = int_of_float (now -. started) in
67 67 value##.textContent :=
68 68 Js.some (Js.string (format_elapsed elapsed))))
69 69
@@ -149,7 +149,8 @@
149 149 ignore (Js.Unsafe.meth_call modal "showModal" [||])
150 150 | None -> ()
151 151
152 Removed: let replace selector html =
152 Added: let replace_app html =
153 Added: let selector = app_selector in
153 154 let next = Dom_html.createDiv document in
154 155 next##.innerHTML := Js.string html;
155 156 match (query_one document selector, query_one next selector) with
@@ -177,26 +178,24 @@
177 178 true
178 179 | _ -> false
179 180
180 Removed: let replace_app = replace app_selector
181 Removed: let replace_workout = replace workout_selector
182 Removed:
183 181 let push_url url =
184 182 Dom_html.window##.history##pushState
185 183 Js.null (Js.string "")
186 184 (Js.some (Js.string url))
187 185
188 Removed: let get ?(push = false) ?(workout = false) url =
186 Added: let get ?(push = false) url =
189 187 incr request_number;
190 188 let request = !request_number in
191 189 set_busy true;
192 190 announce "Loading";
193 191 Js_of_ocaml_lwt.XmlHttpRequest.perform_raw_url ~with_credentials:true url
194 192 >>= fun response ->
195 Removed: let replace = if workout then replace_workout else replace_app in
196 193 if request <> !request_number then Lwt.return_unit
197 194 else (
198 195 set_busy false;
199 Removed: if response.code >= 200 && response.code < 300 && replace response.content
196 Added: if
197 Added: response.code >= 200 && response.code < 300
198 Added: && replace_app response.content
200 199 then (
201 200 if push then push_url url;
202 201 Lwt.return_unit)
@@ -217,21 +216,14 @@
217 216 let link_click event =
218 217 if event##.button <> 0 || modified event then Js._true
219 218 else
220 Removed: match
221 Removed: closest "a[data-hito-app-link], a[data-hito-workout-link]"
222 Removed: (Dom_html.eventTarget event)
223 Removed: with
219 Added: match closest "a[data-hito-app-link]" (Dom_html.eventTarget event) with
224 220 | None -> Js._true
225 221 | Some link -> (
226 222 match href link with
227 223 | None -> Js._true
228 224 | Some url ->
229 225 Dom.preventDefault event;
230 Removed: let workout =
231 Removed: Js.to_bool
232 Removed: (link##hasAttribute (Js.string "data-hito-workout-link"))
233 Removed: in
234 Removed: Lwt.async (fun () -> get ~push:true ~workout url);
226 Added: Lwt.async (fun () -> get ~push:true url);
235 227 Js._false)
236 228
237 229 let form_submit event =
@@ -445,36 +437,37 @@
445 437 (Js.number_of_float 0.));
446 438 Js._true
447 439
448 Removed: (* The feedback modal uses the same native dialog behavior as workout
449 Removed: cancellation. The trigger remains a normal link, so no-script clients open
450 Removed: the server-rendered modal page instead, and Close is a link back to the
451 Removed: logbook that the client turns into an in-place dismiss. The form itself
452 Removed: submits through the shared app-form path. *)
440 Added: (* The feedback modal page renders the dialog with [open]. The client upgrades
441 Added: it to a true modal after each swap. Close is a link back to the logbook that
442 Added: the client turns into an in-place dismiss. The form itself submits through
443 Added: the shared app-form path. *)
453 444 let feedback_click event =
454 Removed: let target = Dom_html.eventTarget event in
455 Removed: match closest "[data-hito-feedback-open]" target with
456 Removed: | Some _ ->
445 Added: match closest "[data-hito-feedback-dismiss]" (Dom_html.eventTarget event) with
446 Added: | Some dismiss ->
457 447 Dom.preventDefault event;
458 Removed: open_feedback_modal ();
448 Added: close_feedback_modal ();
449 Added: (match href dismiss with
450 Added: | Some url ->
451 Added: Dom_html.window##.history##replaceState
452 Added: Js.null (Js.string "")
453 Added: (Js.some (Js.string url))
454 Added: | None -> ());
459 455 Js._false
460 Removed: | None -> (
461 Removed: match closest "[data-hito-feedback-dismiss]" target with
462 Removed: | Some dismiss ->
463 Removed: Dom.preventDefault event;
464 Removed: close_feedback_modal ();
465 Removed: (match href dismiss with
466 Removed: | Some url ->
467 Removed: Dom_html.window##.history##replaceState
468 Removed: Js.null (Js.string "")
469 Removed: (Js.some (Js.string url))
470 Removed: | None -> ());
471 Removed: Js._false
472 Removed: | None -> Js._true)
456 Added: | None -> Js._true
473 457
458 Added: (* One delegated click listener. Every handler sees every click, as separate
459 Added: listeners would. A handler that consumes the click returns [false]. *)
460 Added: let click event =
461 Added: let handled =
462 Added: List.map
463 Added: (fun handler -> Js.to_bool (handler event))
464 Added: [ link_click; feedback_click; dialog_click; confirm_click ]
465 Added: in
466 Added: Js.bool (List.for_all Fun.id handled)
467 Added:
474 468 let () =
475 469 Dom_html.addEventListener document Dom_html.Event.click
476 Removed: (Dom_html.handler link_click)
477 Removed: Js._false
470 Added: (Dom_html.handler click) Js._false
478 471 |> ignore;
479 472 Dom_html.addEventListener document Dom_html.Event.submit
480 473 (Dom_html.handler form_submit)
@@ -487,18 +480,6 @@
487 480 Dom_html.addEventListener document
488 481 (Dom_html.Event.make "reset")
489 482 (Dom_html.handler correction_reset)
490 Removed: Js._false
491 Removed: |> ignore;
492 Removed: Dom_html.addEventListener document Dom_html.Event.click
493 Removed: (Dom_html.handler feedback_click)
494 Removed: Js._false
495 Removed: |> ignore;
496 Removed: Dom_html.addEventListener document Dom_html.Event.click
497 Removed: (Dom_html.handler dialog_click)
498 Removed: Js._false
499 Removed: |> ignore;
500 Removed: Dom_html.addEventListener document Dom_html.Event.click
501 Removed: (Dom_html.handler confirm_click)
502 483 Js._false
503 484 |> ignore;
504 485 Dom_html.addEventListener Dom_html.window Dom_html.Event.popstate