feat Add mobile logbook theme

Apply an accessible Work Sans and Courier Prime ledger theme with workout-aware sticky mobile navigation and labeled logging controls.

Commit
ac76e0ec50ea2abce6425dcfe5e1c8aed5c8c2e3
Author
Marius Peter <dev@marius-peter.com>
Author date
Committer
Marius Peter <dev@marius-peter.com>
Committer date
Changed files
lib/web/assets/hito.css
index 373cd7eb..f345a493 100644..100644
@@ -1,49 +1,72 @@
1 Added: @import url("https://fonts.googleapis.com/css2?family=Courier+Prime:wght@400;700&family=Work+Sans:ital,wght@0,400;0,500;0,600;0,700;0,800;1,400;1,600&display=swap");
2 Added:
1 3 :root {
2 Removed: color-scheme: dark;
3 Removed: --ink: #17191d;
4 Removed: --panel: #202328;
5 Removed: --panel-raised: #292d33;
6 Removed: --line: #464b54;
7 Removed: --muted: #afb2b8;
8 Removed: --bone: #f1eee8;
9 Removed: --accent: #d9877b;
10 Removed: --accent-strong: #a84438;
11 Removed: --brass: #c69a56;
12 Removed: --danger: #d77b70;
13 Removed: --shadow: 0 1.5rem 4rem rgba(0, 0, 0, 0.28);
4 Added: color-scheme: light;
5 Added: --paper: #f7f2e8;
6 Added: --paper-raised: #fffdf8;
7 Added: --ink: #1a1a16;
8 Added: --muted-ink: #5b584f;
9 Added: --rule: #b9b1a2;
10 Added: --rule-strong: #756f63;
11 Added: --oxblood: #8c201b;
12 Added: --oxblood-dark: #65130f;
13 Added: --brass: #7b5714;
14 Added: --brass-wash: #f4e7c5;
15 Added: --evergreen: #1f5b45;
16 Added: --evergreen-wash: #dcebe2;
17 Added: --focus: #005fcc;
18 Added: --error: #8c201b;
19 Added: --font-size-small: 0.875rem;
20 Added: --font-size-base: 1rem;
21 Added: --font-size-lead: 1.125rem;
22 Added: --font-size-heading: 2rem;
23 Added: --sans: "Work Sans", ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
24 Added: --mono: "Courier Prime", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
14 25 }
15 26
16 Removed: * {
17 Removed: box-sizing: border-box;
18 Removed: }
27 Added: * { box-sizing: border-box; }
19 28
29 Added: html { background: var(--paper); }
30 Added:
20 31 body {
21 32 min-width: 20rem;
22 33 margin: 0;
23 Removed: color: var(--bone);
24 Removed: background:
25 Removed: radial-gradient(circle at 14% -10%, #343840 0, transparent 27rem),
26 Removed: radial-gradient(circle at 94% 12%, #39343a 0, transparent 24rem),
27 Removed: var(--ink);
28 Removed: font:
29 Removed: 16px/1.55 Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,
30 Removed: "Segoe UI", sans-serif;
34 Added: color: var(--ink);
35 Added: background: var(--paper);
36 Added: font: 400 var(--font-size-base)/1.5 var(--sans);
37 Added: text-rendering: optimizeLegibility;
31 38 }
32 39
40 Added: body::before {
41 Added: position: fixed;
42 Added: z-index: -1;
43 Added: inset: 0;
44 Added: background-image: linear-gradient(to bottom, rgba(26, 26, 22, 0.035) 1px, transparent 1px);
45 Added: background-size: 100% 2rem;
46 Added: content: "";
47 Added: pointer-events: none;
48 Added: }
49 Added:
33 50 a {
34 Removed: color: var(--accent);
35 Removed: text-decoration-thickness: 0.09em;
36 Removed: text-underline-offset: 0.18em;
51 Added: color: var(--oxblood-dark);
52 Added: font-weight: 600;
53 Added: text-decoration-thickness: 0.1em;
54 Added: text-underline-offset: 0.16em;
37 55 }
38 56
39 Removed: a:hover {
40 Removed: color: #efb0a7;
57 Added: a:hover { color: var(--oxblood); }
58 Added:
59 Added: a:focus-visible,
60 Added: input:focus-visible,
61 Added: select:focus-visible {
62 Added: outline: 3px solid var(--focus);
63 Added: outline-offset: 3px;
41 64 }
42 65
43 66 .app-shell {
44 Removed: width: min(100%, 68rem);
67 Added: width: min(100%, 72rem);
45 68 margin: 0 auto;
46 Removed: padding: 1.25rem;
69 Added: padding: 0 1rem 2.5rem;
47 70 }
48 71
49 72 .masthead {
@@ -51,422 +74,284 @@
51 74 align-items: center;
52 75 justify-content: space-between;
53 76 gap: 1rem;
54 Removed: padding: 1rem 0 1.25rem;
55 Removed: border-bottom: 1px solid rgba(241, 238, 227, 0.18);
77 Added: min-height: 4.75rem;
78 Added: border-bottom: 3px solid var(--ink);
56 79 }
57 80
58 81 .brand {
59 Removed: display: flex;
82 Added: display: inline-flex;
60 83 align-items: center;
61 Removed: gap: 0.75rem;
62 Removed: color: var(--bone);
84 Added: gap: 0.7rem;
85 Added: color: var(--ink);
63 86 text-decoration: none;
64 87 }
65 88
66 89 .brand-mark {
67 90 display: grid;
68 Removed: width: 2.5rem;
69 Removed: height: 2.5rem;
91 Added: width: 2.25rem;
92 Added: height: 2.25rem;
70 93 place-items: center;
71 Removed: border: 1px solid var(--accent);
72 Removed: border-radius: 0.7rem 1.15rem 0.7rem 1.15rem;
73 Removed: color: var(--accent);
74 Removed: font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
75 Removed: font-size: 0.72rem;
76 Removed: font-weight: 800;
94 Added: border: 2px solid var(--ink);
95 Added: color: var(--paper-raised);
96 Added: background: var(--oxblood);
97 Added: font: 700 0.78rem/1 var(--mono);
77 98 letter-spacing: 0.08em;
78 99 }
79 100
80 Removed: .brand-copy {
81 Removed: display: grid;
82 Removed: line-height: 1.05;
83 Removed: }
84 Removed:
85 Removed: .brand-copy strong {
86 Removed: font-size: 1.2rem;
87 Removed: letter-spacing: -0.03em;
88 Removed: }
89 Removed:
101 Added: .brand-copy { display: grid; line-height: 1.1; }
102 Added: .brand-copy strong { font-size: 1.125rem; font-weight: 800; letter-spacing: -0.025em; }
90 103 .brand-copy small,
91 Removed: .eyebrow {
92 Removed: color: var(--accent);
93 Removed: font-size: 0.68rem;
94 Removed: font-weight: 800;
95 Removed: letter-spacing: 0.14em;
104 Added: .eyebrow,
105 Added: .ledger-meta {
106 Added: font: 700 var(--font-size-small)/1.35 var(--mono);
107 Added: letter-spacing: 0.09em;
96 108 text-transform: uppercase;
97 109 }
110 Added: .brand-copy small, .eyebrow { color: var(--oxblood-dark); }
98 111
99 112 .primary-nav {
100 113 display: flex;
101 Removed: align-items: center;
114 Added: flex-wrap: wrap;
115 Added: justify-content: flex-end;
102 116 gap: 0.2rem;
103 117 }
104 118
105 Removed: .primary-nav > a,
106 Removed: .ot-dropdown-button {
119 Added: .primary-nav > a {
107 120 display: inline-flex;
108 121 align-items: center;
109 Removed: gap: 0.45rem;
110 Removed: min-height: 2.5rem;
122 Added: min-height: 2.75rem;
111 123 padding: 0.55rem 0.7rem;
112 Removed: border-radius: 0.55rem;
113 Removed: color: var(--bone);
114 Removed: font-size: 0.86rem;
115 Removed: font-weight: 700;
124 Added: color: var(--ink);
125 Added: font-size: 0.88rem;
116 126 text-decoration: none;
117 127 }
118 128
119 Removed: .primary-nav > a:hover,
120 Removed: .ot-dropdown:hover {
121 Removed: background: rgba(217, 135, 123, 0.12);
122 Removed: color: var(--accent);
129 Added: .primary-nav > a:hover {
130 Added: background: #ece4d5;
131 Added: color: var(--oxblood-dark);
123 132 }
124 133
125 Removed: .nav-menu {
126 Removed: display: none;
134 Added: .page-home .primary-nav > a[href="/"],
135 Added: .page-routine .primary-nav > a[href="/routine"],
136 Added: .page-workout .primary-nav > a[href="/workout"],
137 Added: .page-history .primary-nav > a[href="/history"] {
138 Added: color: var(--paper-raised);
139 Added: background: var(--oxblood);
127 140 }
128 141
129 Removed: /* Compact navigation for small screens. */
130 Removed: .ot-dropdown {
131 Removed: position: relative;
132 Removed: transition: background-color 0.2s ease;
142 Added: .primary-nav > a[aria-current] {
143 Added: font-weight: 800;
133 144 }
134 145
135 Removed: .ot-dropdown-button {
136 Removed: position: relative;
137 Removed: z-index: 5;
138 Removed: cursor: default;
146 Added: .page-home .bottom-nav > a[href="/"],
147 Added: .page-routine .bottom-nav > a[href="/routine"],
148 Added: .page-workout .bottom-nav > a[href="/workout"],
149 Added: .page-history .bottom-nav > a[href="/history"] {
150 Added: color: var(--paper-raised);
151 Added: background: var(--oxblood);
139 152 }
140 153
141 Removed: .ot-dropdown-background {
142 Removed: position: fixed;
143 Removed: inset: 0;
144 Removed: z-index: 1;
145 Removed: visibility: hidden;
146 Removed: background: #000;
147 Removed: opacity: 0;
148 Removed: transition: opacity 0.15s, visibility 0.15s;
149 Removed: }
154 Added: main { padding: 1.5rem 0; }
150 155
151 Removed: .ot-dropdown-menu {
152 Removed: position: absolute;
153 Removed: top: calc(100% + 0.35rem);
154 Removed: right: 0;
155 Removed: z-index: 10;
156 Removed: display: grid;
157 Removed: min-width: 12rem;
158 Removed: overflow: hidden;
159 Removed: visibility: hidden;
160 Removed: border: 1px solid var(--line);
161 Removed: border-radius: 0.7rem;
162 Removed: background: #262a30;
163 Removed: box-shadow: var(--shadow);
164 Removed: opacity: 0;
165 Removed: transition: opacity 0.2s, visibility 0.2s;
166 Removed: }
156 Added: .bottom-nav { display: none; }
167 157
168 Removed: .ot-dropdown:hover > .ot-dropdown-background {
169 Removed: visibility: visible;
170 Removed: opacity: 0.42;
158 Added: .page-surface {
159 Added: min-height: 26rem;
160 Added: border: 1px solid var(--rule-strong);
161 Added: border-top: 6px solid var(--oxblood);
162 Added: background: var(--paper-raised);
163 Added: border-radius: 0.45rem;
164 Added: box-shadow: 0 0.4rem 1.5rem rgba(26, 26, 22, 0.09);
165 Added: padding: clamp(1.25rem, 3vw, 2rem);
171 166 }
172 167
173 Removed: .ot-dropdown:hover > .ot-dropdown-menu {
174 Removed: visibility: visible;
175 Removed: opacity: 1;
176 Removed: }
177 Removed:
178 Removed: .ot-dropdown-menu a {
179 Removed: padding: 0.8rem 1rem;
180 Removed: color: var(--bone);
181 Removed: font-size: 0.9rem;
168 Added: h2 {
169 Added: max-width: 38rem;
170 Added: margin: 0 0 0.45rem;
171 Added: color: var(--ink);
172 Added: font-size: clamp(1.75rem, 4vw, var(--font-size-heading));
182 173 font-weight: 700;
183 Removed: text-decoration: none;
174 Added: line-height: 1.2;
175 Added: letter-spacing: -0.03em;
184 176 }
185 177
186 Removed: .ot-dropdown-menu a + a {
187 Removed: border-top: 1px solid var(--line);
188 Removed: }
178 Added: h3 { margin: 0; color: var(--ink); font-size: 1.5rem; line-height: 1.2; }
189 179
190 Removed: .ot-dropdown-menu a:hover {
191 Removed: background: rgba(217, 135, 123, 0.13);
192 Removed: color: var(--accent);
193 Removed: }
180 Added: p { max-width: 46rem; margin: 0.65rem 0; color: var(--muted-ink); }
181 Added: h2 + p { font-size: var(--font-size-lead); }
194 182
195 Removed: /* Navigation icon styling. */
196 Removed: .ot-icon {
197 Removed: display: inline-block;
198 Removed: width: 1em;
199 Removed: height: 1em;
200 Removed: overflow: hidden;
201 Removed: position: relative;
183 Added: .ledger-meta {
184 Added: color: var(--muted-ink);
185 Added: font-variant-numeric: tabular-nums;
202 186 }
203 187
204 Removed: .ot-icon::before,
205 Removed: .ot-icon::after {
206 Removed: box-sizing: border-box;
207 Removed: display: inline-block;
208 Removed: content: "";
188 Added: .doctrine-note {
189 Added: max-width: 46rem;
190 Added: margin: 1.25rem 0;
191 Added: border-left: 4px solid var(--brass);
192 Added: color: #47350f;
193 Added: background: var(--brass-wash);
194 Added: font-size: 0.95rem;
195 Added: padding: 0.85rem 1rem;
209 196 }
210 197
211 Removed: .ot-icon-gear::before {
212 Removed: width: 1em;
213 Removed: height: 1em;
214 Removed: border: 0.18em dotted currentColor;
215 Removed: border-radius: 50%;
216 Removed: }
217 Removed:
218 Removed: .ot-icon-gear::after {
219 Removed: position: absolute;
220 Removed: top: 0.22em;
221 Removed: left: 0.22em;
222 Removed: width: 0.56em;
223 Removed: height: 0.56em;
224 Removed: border: 0.17em solid currentColor;
225 Removed: border-radius: 50%;
226 Removed: }
227 Removed:
228 Removed: main {
229 Removed: padding: 1.65rem 0 2rem;
230 Removed: }
231 Removed:
232 Removed: /* Ot_popup.hcf supplies the structural header/content/footer card wrapper. */
233 Removed: .ot-hcf {
234 Removed: display: flex;
235 Removed: width: 100%;
236 Removed: flex-direction: column;
237 Removed: overflow: hidden;
238 Removed: }
239 Removed:
240 Removed: .ot-hcf-header,
241 Removed: .ot-hcf-footer {
242 Removed: flex-shrink: 0;
243 Removed: }
244 Removed:
245 Removed: .ot-hcf-header:empty,
246 Removed: .ot-hcf-footer:empty {
247 Removed: display: none;
248 Removed: }
249 Removed:
250 Removed: .ot-hcf-content {
251 Removed: flex-grow: 1;
252 Removed: overflow: auto;
253 Removed: }
254 Removed:
255 Removed: .page-surface {
256 Removed: min-height: 30rem;
257 Removed: border: 1px solid var(--line);
258 Removed: border-radius: 1.15rem;
259 Removed: background: linear-gradient(145deg, rgba(42, 45, 51, 0.97), rgba(25, 27, 31, 0.98));
260 Removed: box-shadow: var(--shadow);
261 Removed: }
262 Removed:
263 Removed: .page-surface .ot-hcf-content {
264 Removed: padding: clamp(1.2rem, 4vw, 2.4rem);
265 Removed: }
266 Removed:
267 Removed: h2 {
268 Removed: max-width: 35rem;
269 Removed: margin: 0 0 0.4rem;
270 Removed: font-size: clamp(1.65rem, 5vw, 2.55rem);
271 Removed: line-height: 1.04;
272 Removed: letter-spacing: -0.05em;
273 Removed: }
274 Removed:
275 Removed: h3 {
276 Removed: margin: 0;
277 Removed: color: var(--bone);
278 Removed: font-size: 1rem;
279 Removed: }
280 Removed:
281 Removed: p {
282 Removed: max-width: 45rem;
283 Removed: margin: 0.55rem 0;
284 Removed: color: var(--muted);
285 Removed: }
286 Removed:
287 Removed: h2 + p {
288 Removed: max-width: 39rem;
289 Removed: font-size: 1.03rem;
290 Removed: }
291 Removed:
292 198 fieldset {
293 Removed: margin: 1.15rem 0 0;
294 Removed: padding: 1rem;
295 Removed: border: 1px solid var(--line);
296 Removed: border-radius: 0.8rem;
297 Removed: background: rgba(9, 10, 12, 0.3);
199 Added: margin: 1rem 0 0;
200 Added: padding: 0.875rem;
201 Added: border: 1px solid var(--rule);
202 Added: border-top: 3px solid var(--ink);
203 Added: background: var(--paper);
204 Added: border-radius: 0.3rem;
298 205 }
299 206
300 207 legend {
301 Removed: padding: 0 0.45rem;
302 Removed: color: var(--accent);
303 Removed: font-size: 0.79rem;
304 Removed: font-weight: 800;
305 Removed: letter-spacing: 0.05em;
208 Added: max-width: 100%;
209 Added: padding: 0 0.35rem;
210 Added: color: var(--ink);
211 Added: font: 700 var(--font-size-small)/1.35 var(--mono);
212 Added: letter-spacing: 0.025em;
306 213 }
307 214
215 Added: .field { margin-top: 1rem; }
308 216 label {
309 217 display: block;
310 Removed: margin: 0.85rem 0 0.3rem;
311 Removed: color: var(--bone);
312 Removed: font-size: 0.78rem;
313 Removed: font-weight: 800;
314 Removed: letter-spacing: 0.04em;
315 Removed: text-transform: uppercase;
218 Added: margin: 0 0 0.35rem;
219 Added: color: var(--ink);
220 Added: font-size: var(--font-size-small);
221 Added: font-weight: 700;
316 222 }
317 223
318 Removed: input,
319 Removed: select {
224 Added: input, select {
320 225 width: 100%;
321 Removed: min-height: 2.7rem;
322 Removed: border: 1px solid #5c626c;
323 Removed: border-radius: 0.55rem;
324 Removed: background: rgba(8, 9, 11, 0.5);
325 Removed: color: var(--bone);
326 Removed: font: inherit;
327 Removed: outline: none;
328 Removed: padding: 0.55rem 0.7rem;
226 Added: min-height: 3rem;
227 Added: border: 1px solid var(--rule-strong);
228 Added: border-radius: 0.25rem;
229 Added: color: var(--ink);
230 Added: background: var(--paper-raised);
231 Added: font: 500 1rem/1.3 var(--sans);
232 Added: padding: 0.65rem 0.75rem;
329 233 }
330 234
331 Removed: input:focus,
332 Removed: select:focus {
333 Removed: border-color: var(--accent);
334 Removed: box-shadow: 0 0 0 3px rgba(217, 135, 123, 0.16);
335 Removed: }
235 Added: input:invalid { border-color: var(--error); }
236 Added: input[type="number"] { font-family: var(--mono); font-variant-numeric: tabular-nums; }
336 237
337 Removed: input[type="checkbox"] {
338 Removed: width: auto;
339 Removed: min-height: auto;
340 Removed: margin-right: 0.45rem;
341 Removed: accent-color: var(--accent-strong);
342 Removed: }
343 Removed:
344 238 input[type="submit"] {
345 Removed: width: auto;
346 Removed: min-height: 2.8rem;
239 Added: width: 100%;
240 Added: min-height: 3rem;
347 241 margin-top: 1rem;
348 Removed: border: 0;
349 Removed: border-radius: 0.6rem;
350 Removed: background: var(--accent-strong);
351 Removed: color: #fffaf7;
242 Added: border: 2px solid var(--oxblood-dark);
243 Added: border-radius: 0.3rem;
244 Added: color: var(--paper-raised);
245 Added: background: var(--oxblood);
352 246 cursor: pointer;
353 Removed: font-weight: 900;
354 Removed: letter-spacing: 0.01em;
355 Removed: padding: 0.65rem 1rem;
356 Removed: transition: transform 0.15s ease, background 0.15s ease;
247 Added: font: 700 var(--font-size-small)/1 var(--sans);
248 Added: letter-spacing: 0.055em;
249 Added: text-transform: uppercase;
250 Added: transition: background-color 150ms ease, transform 150ms ease;
357 251 }
358 252
359 253 input[type="submit"]:hover {
360 Removed: background: #c45b4e;
361 Removed: transform: translateY(-1px);
254 Added: background: var(--oxblood-dark);
255 Added: transform: translateY(-2px);
362 256 }
363 Removed:
364 257 input[type="submit"]:disabled {
365 Removed: background: #5a5d63;
366 Removed: color: #ddd9d2;
258 Added: border-color: var(--rule-strong);
259 Added: color: var(--muted-ink);
260 Added: background: #ded8cc;
367 261 cursor: not-allowed;
368 Removed: transform: none;
369 262 }
370 263
371 Removed: .row {
372 Removed: display: flex;
373 Removed: gap: 0.75rem;
374 Removed: }
264 Added: .row { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0.8rem; }
375 265
376 Removed: .row > div {
377 Removed: flex: 1;
378 Removed: min-width: 0;
379 Removed: }
380 Removed:
381 266 .warn {
382 267 max-width: 47rem;
383 268 margin: 1rem 0;
384 Removed: border: 1px solid rgba(198, 154, 86, 0.58);
385 Removed: border-left: 0.28rem solid var(--brass);
386 Removed: border-radius: 0.65rem;
387 Removed: background: rgba(198, 154, 86, 0.1);
388 Removed: color: #eed5aa;
389 Removed: padding: 0.85rem 1rem;
269 Added: border: 1px solid var(--brass);
270 Added: border-left: 5px solid var(--brass);
271 Added: color: #47350f;
272 Added: background: var(--brass-wash);
273 Added: padding: 0.9rem 1rem;
390 274 }
391 275
392 276 .done {
393 Removed: color: #d8d5ce;
394 Removed: font-size: 0.86rem;
395 Removed: font-weight: 700;
277 Added: color: var(--evergreen);
278 Added: font: 700 0.85rem/1.4 var(--mono);
279 Added: font-variant-numeric: tabular-nums;
280 Added: letter-spacing: 0.02em;
396 281 }
397 282
398 Removed: ul {
399 Removed: margin: 1rem 0;
400 Removed: padding: 0;
401 Removed: list-style: none;
402 Removed: }
403 Removed:
283 Added: ul { margin: 1rem 0; padding: 0; list-style: none; }
404 284 li {
405 285 position: relative;
406 Removed: margin: 0.45rem 0;
407 Removed: padding: 0.75rem 0.85rem 0.75rem 1.35rem;
408 Removed: border-left: 1px solid var(--line);
409 Removed: background: rgba(255, 255, 255, 0.025);
286 Added: margin: 0;
287 Added: padding: 0.8rem 0.85rem 0.8rem 1.25rem;
288 Added: border-top: 1px solid var(--rule);
289 Added: color: var(--ink);
290 Added: background: var(--paper-raised);
410 291 }
411 Removed:
412 292 li::before {
413 293 position: absolute;
414 Removed: top: 1.28rem;
294 Added: top: 1.2rem;
415 295 left: 0.55rem;
416 Removed: width: 0.3rem;
417 Removed: height: 0.3rem;
418 Removed: border-radius: 50%;
419 Removed: background: var(--accent);
296 Added: width: 0.35rem;
297 Added: height: 0.35rem;
298 Added: background: var(--evergreen);
420 299 content: "";
421 300 }
301 Added: li ul { margin: 0.55rem 0 0; }
302 Added: li li { border-top-color: #ded8cc; color: var(--muted-ink); font-size: 0.92rem; }
422 303
423 Removed: li ul {
424 Removed: margin: 0.55rem 0 0;
425 Removed: }
426 Removed:
427 Removed: li li {
428 Removed: border-left-color: rgba(164, 180, 170, 0.28);
429 Removed: color: var(--muted);
430 Removed: font-size: 0.92rem;
431 Removed: }
432 Removed:
433 304 @media (max-width: 42rem) {
434 305 .app-shell {
435 Removed: padding: 0.85rem;
306 Added: padding: 0 0.75rem calc(6.75rem + env(safe-area-inset-bottom));
436 307 }
437 308
438 309 .masthead {
439 Removed: align-items: flex-start;
310 Added: min-height: 4.5rem;
311 Added: padding: 0.9rem 0 0.7rem;
440 312 }
441 313
442 Removed: .primary-nav > a {
443 Removed: display: none;
444 Removed: }
314 Added: .primary-nav { display: none; }
445 315
446 Removed: .nav-menu {
447 Removed: display: block;
316 Added: .bottom-nav {
317 Added: position: fixed;
318 Added: z-index: 20;
319 Added: right: 0;
320 Added: bottom: 0;
321 Added: left: 0;
322 Added: display: grid;
323 Added: grid-template-columns: repeat(3, minmax(0, 1fr));
324 Added: gap: 1px;
325 Added: border-top: 2px solid var(--ink);
326 Added: background: var(--ink);
327 Added: box-shadow: 0 -0.35rem 1.2rem rgba(26, 26, 22, 0.15);
328 Added: padding-bottom: env(safe-area-inset-bottom);
448 329 }
449 330
450 Removed: .page-surface {
451 Removed: min-height: calc(100vh - 8.25rem);
452 Removed: border-radius: 0.9rem;
331 Added: .bottom-nav > a {
332 Added: display: grid;
333 Added: min-height: 3.5rem;
334 Added: place-items: center;
335 Added: color: var(--ink);
336 Added: background: var(--paper-raised);
337 Added: font: 700 0.76rem/1.1 var(--sans);
338 Added: letter-spacing: 0.025em;
339 Added: text-align: center;
340 Added: text-decoration: none;
453 341 }
454 342
455 Removed: .page-surface .ot-hcf-content {
456 Removed: padding: 1.1rem;
343 Added: .bottom-nav > a:hover {
344 Added: color: var(--oxblood-dark);
345 Added: background: #ece4d5;
457 346 }
458 347
459 Removed: .row {
460 Removed: gap: 0.55rem;
461 Removed: }
348 Added: .page-surface { min-height: calc(100vh - 9rem); padding: 1.25rem; }
462 349 }
463 350
464 351 @media (max-width: 26rem) {
465 Removed: .row {
466 Removed: display: block;
467 Removed: }
352 Added: .row { grid-template-columns: 1fr; }
353 Added: }
468 354
469 Removed: .row > div + div {
470 Removed: margin-top: 0.75rem;
471 Removed: }
355 Added: @media (prefers-reduced-motion: reduce) {
356 Added: *, *::before, *::after { scroll-behavior: auto !important; transition-duration: 0.01ms !important; animation-duration: 0.01ms !important; }
472 357 }
lib/web/pages.ml
index 36b5d374..ac052be1 100644..100644
@@ -13,7 +13,17 @@
13 13 let step = Dream_html.string_attr "step"
14 14 let required = Dream_html.attr "required"
15 15
16 Removed: let html_page title content =
16 Added: let html_page ?(active = "") ?(workout_in_progress = false) title content =
17 Added: let mobile_middle =
18 Added: if workout_in_progress then
19 Added: tag "a"
20 Added: [ Dream_html.path_attr (Dream_html.uri_attr "href") Routes.workout ]
21 Added: [ txt "Current Workout" ]
22 Added: else
23 Added: tag "a"
24 Added: [ Dream_html.path_attr (Dream_html.uri_attr "href") Routes.routine ]
25 Added: [ txt "Routine" ]
26 Added: in
17 27 tag "html" []
18 28 [
19 29 tag "head" []
@@ -35,7 +45,7 @@
35 45 ];
36 46 ];
37 47 tag "body"
38 Removed: [ class_ "hito-app" ]
48 Added: [ Dream_html.string_attr "class" "hito-app page-%s" active ]
39 49 [
40 50 tag "div"
41 51 [ class_ "app-shell" ]
@@ -56,7 +66,7 @@
56 66 [ class_ "brand-copy" ]
57 67 [
58 68 tag "strong" [] [ txt "hito" ];
59 Removed: tag "small" [] [ txt "Heavy Duty logbook" ];
69 Added: tag "small" [] [ txt "High Intensity Tracker Online" ];
60 70 ];
61 71 ];
62 72 tag "nav"
@@ -94,6 +104,26 @@
94 104 ];
95 105 tag "main" [] [ tag "div" [ class_ "page-surface" ] content ];
96 106 ];
107 Added: tag "nav"
108 Added: [
109 Added: class_ "bottom-nav";
110 Added: Dream_html.string_attr "aria-label" "Mobile navigation";
111 Added: ]
112 Added: [
113 Added: tag "a"
114 Added: [
115 Added: Dream_html.path_attr (Dream_html.uri_attr "href") Routes.home;
116 Added: ]
117 Added: [ txt "Home" ];
118 Added: mobile_middle;
119 Added: tag "a"
120 Added: [
121 Added: Dream_html.path_attr
122 Added: (Dream_html.uri_attr "href")
123 Added: Routes.history;
124 Added: ]
125 Added: [ txt "Log Book" ];
126 Added: ];
97 127 ];
98 128 ]
99 129
@@ -119,9 +149,9 @@
119 149 Printf.sprintf "%s into %s, no pause — %s" (Exercise.name isolation)
120 150 (Exercise.name compound) window
121 151
122 Removed: let extension_select () =
152 Added: let extension_select ~input_id =
123 153 tag "select"
124 Removed: [ name "extension" ]
154 Added: [ name "extension"; Dream_html.string_attr "id" "%s" input_id ]
125 155 [
126 156 tag "option" [ value "" ] [ txt "to positive failure" ];
127 157 tag "option" [ value "forced" ] [ txt "then forced reps" ];
@@ -131,7 +161,7 @@
131 161 ]
132 162
133 163 let choose_routine ~routines =
134 Removed: html_page "Choose a routine"
164 Added: html_page ~active:"home" "Choose a routine"
135 165 [
136 166 tag "h2" [] [ txt "Routines" ];
137 167 tag "div" []
@@ -199,20 +229,29 @@
199 229 [
200 230 txt "Heavy Duty requires recovery before the next workout.";
201 231 ];
232 Added: tag "p"
233 Added: [ class_ "doctrine-note" ]
234 Added: [
235 Added: txt
236 Added: "The stimulus is immediate. Recovery is the work that \
237 Added: follows it.";
238 Added: ];
202 239 begin_form ~routine ~override:true "Begin under override";
203 240 ];
204 241 ] )
205 242 in
206 Removed: html_page "Home"
243 Added: html_page ~active:"home" "Home"
207 244 ([
208 245 tag "h2" [] [ txt "%s" routine_name ];
209 Removed: tag "p" [] [ txt "Next: %s" (Prescription.Workout.name next) ];
210 Removed: tag "p" [] [ txt "%s" status ];
246 Added: tag "p"
247 Added: [ class_ "ledger-meta" ]
248 Added: [ txt "Next: %s" (Prescription.Workout.name next) ];
249 Added: tag "p" [ class_ "ledger-meta" ] [ txt "%s" status ];
211 250 ]
212 251 @ gate)
213 252
214 253 let routine routine =
215 Removed: html_page "Routine"
254 Added: html_page ~active:"routine" "Routine"
216 255 [
217 256 tag "h2" [] [ txt "%s" (Prescription.Routine.name routine) ];
218 257 tag "ul" []
@@ -241,26 +280,40 @@
241 280 [ txt "%s" (Decode.errors_to_text [ (field, error) ]) ];
242 281 ]
243 282
244 Removed: let input_row field label =
245 Removed: tag "div" []
283 Added: let input_row ~input_id field label =
284 Added: tag "div"
285 Added: [ class_ "field" ]
246 286 [
247 Removed: tag "label" [] [ txt "%s" label ];
248 Removed: void "input" [ type_ "number"; name field; step "0.5"; required ];
287 Added: tag "label"
288 Added: [ Dream_html.string_attr "for" "%s" input_id ]
289 Added: [ txt "%s" label ];
290 Added: void "input"
291 Added: [
292 Added: type_ "number";
293 Added: name field;
294 Added: Dream_html.string_attr "id" "%s" input_id;
295 Added: step "0.5";
296 Added: required;
297 Added: ];
249 298 ]
250 299
251 Removed: let form_for_stimulus ~action_path ~prescription ~errors =
300 Added: let form_for_stimulus ~action_path ~slot ~prescription ~errors =
301 Added: let field_id field = Printf.sprintf "slot-%d-%s" slot field in
252 302 let fields =
253 303 match Prescription.Stimulus.delivery prescription with
254 304 | Prescription.Stimulus.Single _ ->
255 Removed: [ input_row "load" "Load (kg)"; input_row "reps" "Reps to failure" ]
305 Added: [
306 Added: input_row ~input_id:(field_id "load") "load" "Load (kg)";
307 Added: input_row ~input_id:(field_id "reps") "reps" "Reps to failure";
308 Added: ]
256 309 | Prescription.Stimulus.Pre_exhaust { isolation; compound } ->
257 310 [
258 311 tag "p" [ class_ "done" ] [ txt "%s" (Exercise.name isolation) ];
259 Removed: input_row "iso_load" "Load (kg)";
260 Removed: input_row "iso_reps" "Reps";
312 Added: input_row ~input_id:(field_id "iso_load") "iso_load" "Load (kg)";
313 Added: input_row ~input_id:(field_id "iso_reps") "iso_reps" "Reps";
261 314 tag "p" [ class_ "done" ] [ txt "%s" (Exercise.name compound) ];
262 Removed: input_row "comp_load" "Load (kg)";
263 Removed: input_row "comp_reps" "Reps";
315 Added: input_row ~input_id:(field_id "comp_load") "comp_load" "Load (kg)";
316 Added: input_row ~input_id:(field_id "comp_reps") "comp_reps" "Reps";
264 317 ]
265 318 in
266 319 tag "form"
@@ -269,7 +322,12 @@
269 322 tag "fieldset" []
270 323 ([ tag "legend" [] [ txt "%s" (describe_prescription prescription) ] ]
271 324 @ fields
272 Removed: @ [ tag "label" [] [ txt "Ending" ]; extension_select () ]
325 Added: @ [
326 Added: tag "label"
327 Added: [ Dream_html.string_attr "for" "%s" (field_id "extension") ]
328 Added: [ txt "Ending" ];
329 Added: extension_select ~input_id:(field_id "extension");
330 Added: ]
273 331 @ List.concat_map
274 332 (fun field -> error_for field errors)
275 333 [
@@ -341,13 +399,31 @@
341 399 in
342 400 let outstanding_section =
343 401 if outstanding = [] then
344 Removed: [ tag "p" [] [ txt "Everything prescribed has been recorded." ] ]
402 Added: [
403 Added: tag "p"
404 Added: [ class_ "done" ]
405 Added: [ txt "Everything prescribed has been recorded." ];
406 Added: tag "p"
407 Added: [ class_ "doctrine-note" ]
408 Added: [
409 Added: txt
410 Added: "The record is evidence. Keep it complete before drawing \
411 Added: conclusions from it.";
412 Added: ];
413 Added: ]
345 414 else
346 415 tag "h2" [] [ txt "Still to do" ]
416 Added: :: tag "p"
417 Added: [ class_ "doctrine-note" ]
418 Added: [
419 Added: txt
420 Added: "Record each effort as it occurred. The logbook does not revise \
421 Added: the fact.";
422 Added: ]
347 423 :: List.map
348 424 (fun (slot, prescription) ->
349 Removed: form_for_stimulus ~action_path:(form_action slot) ~prescription
350 Removed: ~errors)
425 Added: form_for_stimulus ~action_path:(form_action slot) ~slot
426 Added: ~prescription ~errors)
351 427 outstanding
352 428 in
353 429 let finish_section =
@@ -365,7 +441,7 @@
365 441 [ void "input" [ type_ "submit"; value "Finish workout" ] ];
366 442 ]
367 443 in
368 Removed: html_page
444 Added: html_page ~active:"workout" ~workout_in_progress:(Option.is_none record_id)
369 445 (Prescription.Workout.name prescription)
370 446 (override_note
371 447 @ [
@@ -379,7 +455,7 @@
379 455 @ recorded_section @ outstanding_section @ finish_section)
380 456
381 457 let history records =
382 Removed: html_page "History"
458 Added: html_page ~active:"history" "History"
383 459 [
384 460 tag "h2" [] [ txt "History" ];
385 461 (if records = [] then tag "p" [] [ txt "Nothing logged yet." ]
test/test_web.ml
index 1be4dc85..af84def0 100644..100644
@@ -27,7 +27,69 @@
27 27 `Quick,
28 28 fun () ->
29 29 let response = Dream.test (app ()) (Dream.request "") in
30 Removed: Alcotest.(check int) "status" 200 (status response) );
30 Added: Alcotest.(check int) "status" 200 (status response);
31 Added: Alcotest.(check bool)
32 Added: "Overview active class" true
33 Added: (contains ~substring:"page-home" (body response));
34 Added: Alcotest.(check bool)
35 Added: "expanded brand subtitle" true
36 Added: (contains ~substring:"High Intensity Tracker Online"
37 Added: (body response));
38 Added: Alcotest.(check bool)
39 Added: "mobile navigation shell" true
40 Added: (contains ~substring:"bottom-nav" (body response));
41 Added: Alcotest.(check bool)
42 Added: "routine mobile action" true
43 Added: (contains ~substring:">Routine<" (body response));
44 Added: Alcotest.(check bool)
45 Added: "log book mobile action" true
46 Added: (contains ~substring:">Log Book<" (body response)) );
47 Added: ( "stylesheet declares requested fonts and focus rules",
48 Added: `Quick,
49 Added: fun () ->
50 Added: let response =
51 Added: Dream.test (app ()) (Dream.request ~target:"/assets/hito.css" "")
52 Added: in
53 Added: Alcotest.(check int) "stylesheet" 200 (status response);
54 Added: let stylesheet = body response in
55 Added: Alcotest.(check bool)
56 Added: "Work Sans" true
57 Added: (contains ~substring:"Work Sans" stylesheet);
58 Added: Alcotest.(check bool)
59 Added: "Courier Prime" true
60 Added: (contains ~substring:"Courier Prime" stylesheet);
61 Added: Alcotest.(check bool)
62 Added: "font display swap" true
63 Added: (contains ~substring:"display=swap" stylesheet);
64 Added: Alcotest.(check bool)
65 Added: "visible focus" true
66 Added: (contains ~substring:":focus-visible" stylesheet);
67 Added: Alcotest.(check bool)
68 Added: "active workout navigation" true
69 Added: (contains ~substring:".page-workout .primary-nav" stylesheet);
70 Added: Alcotest.(check bool)
71 Added: "42rem mobile breakpoint" true
72 Added: (contains ~substring:"@media (max-width: 42rem)" stylesheet);
73 Added: Alcotest.(check bool)
74 Added: "sticky bottom navigation" true
75 Added: (contains ~substring:".bottom-nav" stylesheet);
76 Added: Alcotest.(check bool)
77 Added: "fixed bottom navigation" true
78 Added: (contains ~substring:"position: fixed" stylesheet);
79 Added: Alcotest.(check bool)
80 Added: "safe area spacing" true
81 Added: (contains ~substring:"safe-area-inset-bottom" stylesheet);
82 Added: Alcotest.(check bool)
83 Added: "Bootstrap small text scale" true
84 Added: (contains ~substring:"--font-size-small: 0.875rem" stylesheet);
85 Added: Alcotest.(check bool)
86 Added: "balanced heading scale" true
87 Added: (contains
88 Added: ~substring:"clamp(1.75rem, 4vw, var(--font-size-heading))"
89 Added: stylesheet);
90 Added: Alcotest.(check bool)
91 Added: "accessible control height" true
92 Added: (contains ~substring:"min-height: 3rem" stylesheet) );
31 93 ( "a new workout exposes outstanding effort forms on Overview",
32 94 `Quick,
33 95 fun () ->
@@ -40,11 +102,26 @@
40 102 Alcotest.(check int) "overview" 200 (status overview);
41 103 let page = body overview in
42 104 Alcotest.(check bool)
105 Added: "Current workout active class" true
106 Added: (contains ~substring:"page-workout" page);
107 Added: Alcotest.(check bool)
108 Added: "current workout mobile action" true
109 Added: (contains ~substring:">Current Workout<" page);
110 Added: Alcotest.(check bool)
43 111 "outstanding heading" true
44 112 (contains ~substring:"Still to do" page);
45 113 Alcotest.(check bool)
46 114 "first effort form" true
47 Removed: (contains ~substring:"/workout/slots/0" page) );
115 Added: (contains ~substring:"/workout/slots/0" page);
116 Added: Alcotest.(check bool)
117 Added: "load label references control" true
118 Added: (contains ~substring:"for=\"slot-0-iso_load\"" page);
119 Added: Alcotest.(check bool)
120 Added: "load control has ID" true
121 Added: (contains ~substring:"id=\"slot-0-iso_load\"" page);
122 Added: Alcotest.(check bool)
123 Added: "ending label references control" true
124 Added: (contains ~substring:"for=\"slot-0-extension\"" page) );
48 125 ] );
49 126 ]
50 127