rip-lang 3.10.6 → 3.10.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1130 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Lab Results</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com">
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
+ <link href="https://fonts.googleapis.com/css2?family=Lato:ital,wght@0,400;0,500;0,700;1,400&family=Nothing+You+Could+Do&display=swap" rel="stylesheet">
10
+ <script type="module" src="../dist/rip-ui.min.js"></script>
11
+ <style>
12
+ /* ==========================================================================
13
+ Lab Results — Styles
14
+ Single stylesheet: design tokens, app shell, forms, brochure, print
15
+ ========================================================================== */
16
+
17
+ :root {
18
+ --color-primary: #3f5e53;
19
+ --color-text: #333;
20
+ --color-base: #333;
21
+ --color-body: #f5f5f5;
22
+ --color-success: #2EAB27;
23
+ --color-danger: #FB2E47;
24
+ --color-border: #e5e5e5;
25
+ --font-base: 'Lato', sans-serif;
26
+ --font-heading: 'Nothing You Could Do', cursive;
27
+ --radius: 4px;
28
+ --page-width: 800px;
29
+ --page-height: 1056px;
30
+ --shadow-panel: 0 6px 20px 0 rgba(50,50,93,.06), 0 2px 4px 0 rgba(0,0,0,.03);
31
+ }
32
+
33
+ *, *::before, *::after { box-sizing: border-box; }
34
+
35
+ html { font-size: 10px; }
36
+ @media (max-width: 1200px) { html { font-size: 9px; } }
37
+
38
+ body {
39
+ margin: 0;
40
+ min-height: 100vh;
41
+ font-family: var(--font-base);
42
+ font-size: 1.6rem;
43
+ line-height: 1.4;
44
+ color: var(--color-base);
45
+ background: var(--color-body);
46
+ -webkit-font-smoothing: antialiased;
47
+ }
48
+
49
+ /* --- App Shell --- */
50
+
51
+ .app { padding-bottom: 2rem; }
52
+
53
+ @media (min-width: 1300px) {
54
+ .app {
55
+ padding: 3rem;
56
+ display: flex;
57
+ justify-content: center;
58
+ align-items: flex-start;
59
+ }
60
+ }
61
+
62
+ .sidebar {
63
+ display: none;
64
+ background: white;
65
+ padding: 3rem;
66
+ position: sticky;
67
+ top: 3rem;
68
+ box-shadow: var(--shadow-panel);
69
+ border-radius: var(--radius);
70
+ max-width: 40rem;
71
+ max-height: 90vh;
72
+ overflow-y: auto;
73
+ }
74
+
75
+ @media (min-width: 1300px) { .sidebar { display: block; } }
76
+
77
+ .sidebar__heading {
78
+ font-size: 1.8rem;
79
+ font-weight: 600;
80
+ margin-bottom: 2.5rem;
81
+ }
82
+
83
+ .header {
84
+ position: fixed;
85
+ top: 0; left: 0; right: 0;
86
+ display: flex;
87
+ justify-content: space-between;
88
+ align-items: center;
89
+ padding: 1rem 0;
90
+ background: white;
91
+ box-shadow: 0 1px 8px rgba(0,0,0,.15);
92
+ z-index: 10;
93
+ height: 5rem;
94
+ }
95
+
96
+ @media (min-width: 1300px) { .header { display: none; } }
97
+
98
+ .header__toggle {
99
+ padding: 0.5rem 1rem;
100
+ margin-left: 1rem;
101
+ cursor: pointer;
102
+ font-size: 1.9rem;
103
+ }
104
+
105
+ .header__title { font-size: 1.6rem; font-weight: 600; }
106
+
107
+ .drawer-overlay {
108
+ display: none;
109
+ position: fixed;
110
+ inset: 0;
111
+ background: rgba(0,0,0,.4);
112
+ z-index: 20;
113
+ }
114
+
115
+ .drawer-overlay.open { display: block; }
116
+
117
+ .drawer {
118
+ position: fixed;
119
+ top: 0; left: 0; bottom: 0;
120
+ width: 320px;
121
+ background: white;
122
+ z-index: 30;
123
+ padding: 2rem;
124
+ overflow-y: auto;
125
+ transform: translateX(-100%);
126
+ transition: transform 0.25s ease;
127
+ }
128
+
129
+ @media (min-width: 550px) { .drawer { width: 400px; } }
130
+
131
+ .drawer.open { transform: translateX(0); }
132
+
133
+ .preview { padding-top: 6rem; }
134
+
135
+ @media (min-width: 1300px) {
136
+ .preview { padding: 0; margin-left: 4rem; }
137
+ }
138
+
139
+ /* --- Settings Form --- */
140
+
141
+ .settings__section { margin-top: 3rem; }
142
+ .settings__section:first-child { margin-top: 0; }
143
+
144
+ .settings__heading {
145
+ font-weight: 600;
146
+ font-size: 1.4rem;
147
+ text-transform: uppercase;
148
+ color: rgba(51,51,51,.8);
149
+ margin-bottom: 1.8rem;
150
+ letter-spacing: 0.3px;
151
+ }
152
+
153
+ .settings__fields {
154
+ display: flex;
155
+ flex-wrap: wrap;
156
+ gap: 1.2rem;
157
+ }
158
+
159
+ .settings__field {
160
+ flex: 1 1 45%;
161
+ min-width: 140px;
162
+ }
163
+
164
+ .settings__field label {
165
+ display: block;
166
+ font-size: 1.3rem;
167
+ font-weight: 500;
168
+ color: rgba(51,51,51,.7);
169
+ margin-bottom: 0.4rem;
170
+ }
171
+
172
+ .settings__field input,
173
+ .settings__field select {
174
+ width: 100%;
175
+ padding: 1rem 1.2rem;
176
+ font-family: var(--font-base);
177
+ font-size: 1.5rem;
178
+ font-weight: 500;
179
+ border: 1px solid var(--color-border);
180
+ border-radius: var(--radius);
181
+ outline: none;
182
+ transition: border-color 0.15s;
183
+ color: var(--color-text);
184
+ background: white;
185
+ }
186
+
187
+ .settings__field input:focus,
188
+ .settings__field select:focus {
189
+ border-color: var(--color-primary);
190
+ }
191
+
192
+ .btn-primary {
193
+ display: block;
194
+ width: 100%;
195
+ margin-top: 3rem;
196
+ padding: 1.2rem;
197
+ font-family: var(--font-base);
198
+ font-size: 1.55rem;
199
+ font-weight: 800;
200
+ color: white;
201
+ background: var(--color-primary);
202
+ border: none;
203
+ border-radius: var(--radius);
204
+ cursor: pointer;
205
+ transition: background 0.15s;
206
+ }
207
+
208
+ .btn-primary:hover { background: #355247; }
209
+
210
+ /* --- Brochure Pages --- */
211
+
212
+ .brochure {
213
+ width: var(--page-width);
214
+ margin: 0 auto;
215
+ box-shadow: var(--shadow-panel);
216
+ }
217
+
218
+ .brochure__page {
219
+ font-size: 15px;
220
+ width: var(--page-width);
221
+ min-height: var(--page-height);
222
+ background-size: cover;
223
+ background-repeat: no-repeat;
224
+ border-top: 2px solid rgba(0,0,0,.1);
225
+ position: relative;
226
+ overflow: hidden;
227
+ }
228
+
229
+ .brochure__page:first-child { border-top: 0; }
230
+
231
+ .page-cover {
232
+ display: flex;
233
+ flex-direction: column;
234
+ justify-content: space-between;
235
+ height: var(--page-height);
236
+ }
237
+
238
+ .cover__heading-fade {
239
+ width: 100%;
240
+ height: 280px;
241
+ background: linear-gradient(white, rgba(255,255,255,0));
242
+ }
243
+
244
+ .cover__title {
245
+ font-family: var(--font-heading);
246
+ font-size: 50px;
247
+ letter-spacing: 1.5px;
248
+ line-height: 60px;
249
+ padding: 60px 0 0 60px;
250
+ width: 380px;
251
+ text-transform: uppercase;
252
+ font-weight: 700;
253
+ }
254
+
255
+ .cover__body {
256
+ background: linear-gradient(rgba(0,0,0,0), rgba(0,0,0,.7));
257
+ flex: 1;
258
+ display: flex;
259
+ flex-direction: column;
260
+ justify-content: flex-end;
261
+ }
262
+
263
+ .cover__copy {
264
+ font-size: 20px;
265
+ margin: 0 0 60px 60px;
266
+ color: white;
267
+ text-shadow: 1px 1px 5px rgba(0,0,0,.3);
268
+ width: 440px;
269
+ }
270
+
271
+ .cover__footer {
272
+ display: flex;
273
+ justify-content: space-between;
274
+ align-items: center;
275
+ padding: 15px 40px;
276
+ background: rgba(255,255,255,.9);
277
+ backdrop-filter: blur(10px);
278
+ }
279
+
280
+ .cover__footer img { width: 135px; }
281
+
282
+ .cover__powered-by {
283
+ opacity: 0.8;
284
+ font-weight: 600;
285
+ color: rgba(51,51,51,.9);
286
+ }
287
+
288
+ .cover__powered-by div:first-child { font-size: 15px; }
289
+ .cover__powered-by div:last-child { font-size: 17px; margin-top: -3px; }
290
+
291
+ .page-summary { padding: 30px; }
292
+
293
+ .page-summary .page-heading {
294
+ color: var(--color-primary);
295
+ font-size: 20px;
296
+ letter-spacing: 0.5px;
297
+ font-weight: 600;
298
+ text-transform: uppercase;
299
+ }
300
+
301
+ .summary__intro {
302
+ margin-top: 30px;
303
+ display: flex;
304
+ justify-content: space-between;
305
+ align-items: center;
306
+ }
307
+
308
+ .summary__patient {
309
+ background: white;
310
+ border: 1px solid rgba(63,94,83,.6);
311
+ padding: 20px;
312
+ border-radius: var(--radius);
313
+ flex-shrink: 0;
314
+ }
315
+
316
+ .summary__patient-name { font-weight: 700; font-size: 17px; }
317
+ .summary__patient-meta { margin-top: 2px; color: rgba(51,51,51,.8); }
318
+
319
+ .summary__patient-lab { margin-top: 15px; color: rgba(51,51,51,.8); }
320
+ .summary__patient-lab strong { font-weight: 600; color: rgba(51,51,51,.9); }
321
+
322
+ .summary__copy {
323
+ flex: 1;
324
+ margin-left: 40px;
325
+ line-height: 1.5;
326
+ color: rgba(51,51,51,.8);
327
+ }
328
+
329
+ .summary-table {
330
+ width: 100%;
331
+ margin-top: 15px;
332
+ border-spacing: 0;
333
+ border-collapse: collapse;
334
+ }
335
+
336
+ .summary-table:first-of-type { margin-top: 20px; }
337
+
338
+ .summary-table thead th {
339
+ font-weight: 800;
340
+ font-size: 12pt;
341
+ padding: 10px 0;
342
+ }
343
+
344
+ .summary-table tbody {
345
+ background: white;
346
+ border: 1px solid #f0f0f0;
347
+ border-radius: var(--radius);
348
+ }
349
+
350
+ .summary-table tbody tr { border-bottom: 1px solid #f0f0f0; }
351
+ .summary-table tbody tr:first-child,
352
+ .summary-table tbody tr:last-child { border: none; }
353
+
354
+ .summary-table tbody tr:first-child td {
355
+ padding-top: 20px;
356
+ padding-bottom: 0;
357
+ }
358
+
359
+ .summary-table tbody tr:first-child td:first-child {
360
+ font-size: 14px;
361
+ font-weight: 700;
362
+ color: rgba(51,51,51,.7);
363
+ text-transform: uppercase;
364
+ letter-spacing: 0.2px;
365
+ }
366
+
367
+ .summary-table td {
368
+ text-align: center;
369
+ padding: 15px 25px;
370
+ }
371
+
372
+ .summary-table td:first-child { text-align: left; width: 320px; }
373
+ .summary-table td:nth-child(2) { font-weight: 600; }
374
+
375
+ .summary-table td .name { font-size: 17px; font-weight: 600; }
376
+ .summary-table td .desc { font-size: 14px; color: rgba(51,51,51,.7); }
377
+
378
+ .summary-table td.value {
379
+ color: rgba(51,51,51,.8);
380
+ font-size: 20pt;
381
+ }
382
+
383
+ .row-acceptable td:first-child,
384
+ .row-acceptable td:nth-child(2) { color: var(--color-success); }
385
+
386
+ .row-unacceptable td:first-child,
387
+ .row-unacceptable td:nth-child(2) { color: var(--color-danger); }
388
+
389
+ .row-acceptable td:first-child,
390
+ .row-unacceptable td:first-child {
391
+ display: flex;
392
+ align-items: center;
393
+ gap: 12px;
394
+ }
395
+
396
+ .status-icon {
397
+ width: 20px;
398
+ height: 20px;
399
+ flex-shrink: 0;
400
+ }
401
+
402
+ .page-detail {
403
+ display: flex;
404
+ flex-direction: column;
405
+ min-height: var(--page-height);
406
+ }
407
+
408
+ .detail__header {
409
+ flex-shrink: 0;
410
+ color: white;
411
+ display: flex;
412
+ justify-content: center;
413
+ align-items: center;
414
+ padding: 30px 40px;
415
+ min-height: 280px;
416
+ }
417
+
418
+ .detail__header-image {
419
+ background-size: contain;
420
+ background-repeat: no-repeat;
421
+ background-position: center;
422
+ flex-shrink: 0;
423
+ }
424
+
425
+ .detail__header-text { margin-left: 70px; max-width: 360px; }
426
+
427
+ .detail__header-title {
428
+ font-size: 18px;
429
+ font-weight: 700;
430
+ text-transform: uppercase;
431
+ }
432
+
433
+ .detail__header-copy {
434
+ color: rgba(255,255,255,.85);
435
+ margin-top: 20px;
436
+ line-height: 1.5;
437
+ white-space: pre-line;
438
+ }
439
+
440
+ .detail__content {
441
+ display: flex;
442
+ padding: 25px;
443
+ flex: 1;
444
+ }
445
+
446
+ .detail__item {
447
+ width: 50%;
448
+ background: white;
449
+ border: 1px solid #f0f0f0;
450
+ border-radius: var(--radius);
451
+ padding: 25px;
452
+ }
453
+
454
+ .detail__item + .detail__item { margin-left: 25px; }
455
+
456
+ .detail__item-header {
457
+ margin-top: 15px;
458
+ display: flex;
459
+ align-items: center;
460
+ gap: 12px;
461
+ }
462
+
463
+ .detail__item-header .name { font-size: 18px; font-weight: 600; }
464
+ .detail__item-header .desc { font-size: 15px; color: rgba(51,51,51,.7); }
465
+
466
+ .detail__item-header.acceptable { color: var(--color-success); }
467
+ .detail__item-header.unacceptable { color: var(--color-danger); }
468
+
469
+ .detail__item-info {
470
+ margin-top: 20px;
471
+ color: rgba(51,51,51,.9);
472
+ line-height: 1.5;
473
+ }
474
+
475
+ .detail__item-info p { margin: 1rem 0; }
476
+ .detail__item-info p:first-child { margin-top: 0; }
477
+ .detail__item-info strong { font-weight: 700; }
478
+
479
+ .detail__maintenance { margin-top: 25px; }
480
+
481
+ .detail__maintenance-heading {
482
+ text-transform: uppercase;
483
+ font-weight: 600;
484
+ font-size: 14px;
485
+ }
486
+
487
+ .detail__maintenance-item {
488
+ margin-top: 15px;
489
+ display: flex;
490
+ align-items: baseline;
491
+ gap: 12px;
492
+ color: rgba(51,51,51,.9);
493
+ line-height: 1.4;
494
+ }
495
+
496
+ .detail__maintenance-icon {
497
+ width: 16px;
498
+ flex-shrink: 0;
499
+ display: flex;
500
+ justify-content: center;
501
+ color: rgba(51,51,51,.5);
502
+ }
503
+
504
+ .detail__maintenance-icon + div {
505
+ max-width: 80%;
506
+ }
507
+
508
+ .detail__footer-image {
509
+ flex: 1;
510
+ min-height: 280px;
511
+ background-size: cover;
512
+ background-repeat: no-repeat;
513
+ background-position: center;
514
+ }
515
+
516
+ .gauge {
517
+ position: relative;
518
+ display: inline-block;
519
+ }
520
+
521
+ .gauge--flip svg { transform: scaleX(-1); }
522
+
523
+ .gauge__needle {
524
+ position: absolute;
525
+ width: 131px;
526
+ height: 131px;
527
+ margin: 0 auto;
528
+ border-radius: 50%;
529
+ top: 21px;
530
+ left: 0;
531
+ right: 0;
532
+ }
533
+
534
+ .gauge__arrow,
535
+ .gauge__arrow-border {
536
+ position: absolute;
537
+ margin: 0 auto;
538
+ left: 0; right: 0;
539
+ width: 0; height: 0;
540
+ }
541
+
542
+ .gauge__arrow {
543
+ top: -13px;
544
+ z-index: 2;
545
+ border-left: 15px solid transparent;
546
+ border-right: 15px solid transparent;
547
+ border-bottom: 15px solid var(--color-success);
548
+ }
549
+
550
+ .gauge__arrow-border {
551
+ top: -15px;
552
+ z-index: 1;
553
+ border-left: 15px solid transparent;
554
+ border-right: 15px solid transparent;
555
+ border-bottom: 15px solid white;
556
+ }
557
+
558
+ .gauge__value,
559
+ .gauge__units {
560
+ font-weight: 600;
561
+ position: absolute;
562
+ left: 0; right: 0;
563
+ text-align: center;
564
+ }
565
+
566
+ .gauge__value { font-size: 18px; color: white; bottom: 26px; }
567
+ .gauge__units { font-size: 13px; color: rgba(255,255,255,.9); bottom: 13px; }
568
+
569
+ @media print {
570
+ body { background: white; }
571
+
572
+ .header, .sidebar, .drawer, .drawer-overlay, .btn-primary,
573
+ .settings__section { display: none !important; }
574
+
575
+ .app { padding: 0; display: block; }
576
+ .preview { padding: 0; margin: 0; }
577
+
578
+ .brochure { width: 100%; box-shadow: none; }
579
+
580
+ .brochure__page {
581
+ width: 100%;
582
+ height: 100%;
583
+ position: absolute;
584
+ top: 0; left: 0;
585
+ margin: auto;
586
+ border: none;
587
+ page-break-before: always;
588
+ }
589
+
590
+ .brochure__page:first-child { page-break-before: auto; }
591
+ }
592
+ </style>
593
+ </head>
594
+ <body>
595
+
596
+ <!-- ===== Components ===== -->
597
+
598
+ <script type="text/rip" data-name="index">
599
+ # Lab Results - Main Page
600
+
601
+ export Home = component
602
+ appTitle := 'Lab Results'
603
+ title := 'My Guide to Health'
604
+ poweredBy := 'Crossover Health'
605
+ lab := 'Medical Diagnostics'
606
+
607
+ firstName := 'Scott'
608
+ lastName := 'Bowman'
609
+ age := 42
610
+ gender := 'Male'
611
+
612
+ history := [
613
+ date: '2022-10'
614
+ triglycerides: 181
615
+ hdlCholesterol: 44
616
+ totalCholesterol: 177
617
+ cholesterolHdlRatio: 4.3
618
+ glucose: 75
619
+ a1c: 5.2
620
+ waistCircumference: 36
621
+ bloodPressure: '118/76'
622
+ ]
623
+
624
+ ranges :=
625
+ triglycerides:
626
+ name: 'Triglycerides'
627
+ range: ['<', 150]
628
+ units: 'mg/dL'
629
+ desc: 'Reference range: < 150 mg/dL'
630
+ info:
631
+ acceptable: 'Your result falls within the normal Reference Range.'
632
+ unacceptable: 'Your result falls outside the normal Reference Range.'
633
+ hdlCholesterol:
634
+ name: 'HDL Cholesterol'
635
+ range: ['>=', 40]
636
+ units: 'mg/dL'
637
+ desc: 'Reference range: ≥ 40 mg/dL'
638
+ info:
639
+ acceptable: 'Your result falls within the normal Reference Range.'
640
+ unacceptable: 'Your result falls outside the normal Reference Range.'
641
+ totalCholesterol:
642
+ name: 'Total Cholesterol'
643
+ range: ['<>', 125, 199]
644
+ units: 'mg/dL'
645
+ desc: 'Reference range: 125-199 mg/dL'
646
+ info:
647
+ acceptable: 'Your result falls within the normal Reference Range.'
648
+ unacceptable: 'Your result falls outside the normal Reference Range.'
649
+ cholesterolHdlRatio:
650
+ name: 'Cholesterol / HDL Ratio'
651
+ range: ['<=', 5]
652
+ units: '(calc)'
653
+ desc: 'Reference range: ≤ 5.0 (calc)'
654
+ info:
655
+ acceptable: 'Your result falls within the normal Reference Range.'
656
+ unacceptable: 'Your result falls outside the normal Reference Range.'
657
+ glucose:
658
+ name: 'Glucose'
659
+ range: ['<>', 65, 99]
660
+ units: 'mg/dL'
661
+ desc: 'Reference range: 65-99 mg/dL'
662
+ info:
663
+ acceptable: 'Your result falls within the normal Reference Range.'
664
+ unacceptable: 'Your result falls outside the normal Reference Range.'
665
+ a1c:
666
+ name: 'Hemoglobin A1c'
667
+ range: ['<', 5.7]
668
+ units: '%'
669
+ desc: 'Reference range: < 5.7%'
670
+ info:
671
+ acceptable: 'Your result falls within the normal Reference Range.'
672
+ unacceptable: 'Your result falls outside the normal Reference Range.'
673
+ waistCircumference:
674
+ name: 'Waist Circumference'
675
+ range: ['<=', 40]
676
+ units: 'in'
677
+ desc: 'Reference range: ≤ 40 in'
678
+ info:
679
+ acceptable: 'Your result falls within the normal Reference Range.'
680
+ unacceptable: 'Your result falls outside the normal Reference Range.'
681
+ bloodPressure:
682
+ name: 'Blood Pressure'
683
+ range: ['<', '120/80']
684
+ units: 'mm/Hg'
685
+ desc: 'Reference range: < 120/80 mmHg'
686
+ info:
687
+ acceptable: 'Your result falls within the normal Reference Range.'
688
+ unacceptable: 'Your result falls outside the normal Reference Range.'
689
+
690
+ acceptable: (key, value) ->
691
+ ref = ranges[key]
692
+ return 'acceptable' unless ref
693
+ r = ref.range
694
+ ok = false
695
+ if typeof r[1] is 'string' and r[1].includes('/')
696
+ bp = r[1].split('/')
697
+ bv = String(value).split('/')
698
+ ok = Number(bv[0]) <= Number(bp[0]) and Number(bv[1]) <= Number(bp[1])
699
+ else
700
+ switch r[0]
701
+ when '<' then ok = value < r[1]
702
+ when '<=' then ok = value <= r[1]
703
+ when '>=' then ok = value >= r[1]
704
+ when '<>' then ok = value >= r[1] and value <= r[2]
705
+ if ok then 'acceptable' else 'unacceptable'
706
+
707
+ topics := [
708
+ { title: 'Heart Health', gradient: 'linear-gradient(45deg, #851717, #E83F3F)', image: 'images/heart.png', imageWidth: 166, imageHeight: 240, copy: "Your heart is one of the most important organs in your body. Every day, it beats around 100,000 times, pumping blood through an extensive network of blood vessels.\n\nIt's responsible for supplying oxygen to your body, removing waste materials, supplying energy and delivering immune system responses. Given all these essential functions, it's important to keep your heart healthy.", biomarkers: [{ key: 'triglycerides', text: '<strong>Triglycerides</strong> are fats composed of fatty acids and glycerol. The level of triglycerides in your blood tells how well your body processes the fat in your diet.', extra: 'Accurate results require fasting for nine to twelve hours (no food or drink except water and medication) prior to testing.', tips: [{ icon: 'utensils', text: 'Drink watery drinks instead of sugary drinks.' }, { icon: 'drop', text: 'Choose fish rich in omega-3 fatty acids to lower your triglycerides.' }] }, { key: 'hdlCholesterol', text: '<strong>High Density Lipoprotein (HDL) cholesterol</strong> is commonly called "good" cholesterol. Unlike other cholesterol levels, the HDL cholesterol test result is best if it is high. Elevated HDL cholesterol is associated with decreased risk of heart disease.', tips: [{ icon: 'utensils', text: 'Try adding almonds or walnuts to hot or cold cereal for extra crunch and some healthy fat.' }, { icon: 'drop', text: 'Choose a margarine spread without hydrogenated or partially-hydrogenated oils.' }] }] }
709
+ { title: 'Heart Health', isImagePage: true, image: 'images/yoga_lady.jpg', biomarkers: [{ key: 'totalCholesterol', text: '<strong>Total Cholesterol</strong> is a combination of three types of cholesterol: HDL, LDL, and part of triglycerides.', extra: 'High cholesterol may put you at risk for heart disease or stroke. A low cholesterol measurement can indicate other health conditions.', tips: [{ icon: 'walk', text: 'Keep it interesting. Try new exercise activities to improve your overall fitness and prevent boredom.' }, { icon: 'utensils', text: 'Choose oatmeal, whole-wheat toast, or a whole-grain English muffin instead of a doughnut or pastry at breakfast.' }] }, { key: 'cholesterolHdlRatio', text: '<strong>Total cholesterol/HDL cholesterol ratio</strong> is a calculation obtained by dividing the total cholesterol level by the HDL cholesterol level and is another indicator of heart disease risk. A ratio of 5.0 or less is associated with a lower risk of heart disease. A ratio of less than 3.5 is highly desirable.', tips: [{ icon: 'utensils', text: 'Go for the whole grains. Try brown rice or whole-wheat pasta. Switch from white bread to whole-wheat bread.' }, { icon: 'drop', text: 'Use liquid fats instead of solid fats (such as shortening) in your cooking and baking.' }] }] }
710
+ { title: 'Diabetes Risk', gradient: 'linear-gradient(45deg, #5C3619, #E8873F)', image: 'images/pancreas.png', imageWidth: 233, imageHeight: 149, copy: "The pancreas is a relatively small organ located right behind your stomach. It has two main functions that help your body convert the food you eat into fuel. The exocrine function aids in digestion while the endocrine function creates and releases hormones to regulate your blood sugar.\n\nBecause of these critical roles, your pancreas can be tied to several serious health issues.", biomarkers: [{ key: 'glucose', text: '<strong>Glucose</strong> ("blood sugar") is the chief source of energy for all cells in the body. Glucose levels are regulated by hormones produced by your pancreas, including insulin.', tips: [{ icon: 'walk', text: 'Boost your metabolism with strength training. Strength training can lower glucose levels by increasing lean muscle and reducing body fat.' }, { icon: 'utensils', text: 'Carbohydrates count. Choose from healthy carbohydrates, such as whole grains, fruits, vegetables, legumes (beans/peas) and low-fat or fat-free milk and yogurt.' }] }, { key: 'a1c', text: "<strong>Hemoglobin A1c</strong> measures the percentage of glucose that's bound to hemoglobin, a protein found in red blood cells whose job is to carry oxygen throughout your body.", tips: [{ icon: 'doctor', text: 'Check your glucose as directed. Work with your doctor to determine if, and how often, you should check your blood sugar.' }, { icon: 'utensils', text: 'Eat a balanced diet. Load up on non-starchy vegetables, but be mindful of serving sizes when eating fruits, protein and carbohydrates.' }] }] }
711
+ { title: 'Physical Measures', gradient: 'linear-gradient(45deg, #214B7A, #3F8EE8)', image: 'images/human_body.png', imageWidth: 140, imageHeight: 247, copy: "During your screening, physical measurements were taken to provide you with more information about your health. These measures are considered risk factors for chronic health conditions, like heart disease, diabetes and stroke.\n\nThese measures should be used with all of your blood tests to understand your risk for these conditions.", conditional: 'waistCircumference', biomarkers: [{ key: 'waistCircumference', text: '<strong>Waist circumference</strong> measures the stored fat around your waist area, also known as "abdominal obesity" or "having an apple shape". It can provide a different look at your weight related health risk than a body mass index (BMI).', tips: [{ icon: 'walk', text: 'Did you know that walking is a great way to reduce belly fat and strengthen the muscles in the lower back?' }, { icon: 'utensils', text: 'Eat at home more and dine out less. Strive to dine out no more than once or twice each week.' }] }, { key: 'bloodPressure', text: '<strong>Blood pressure</strong> is the force of blood pushing against the artery walls as the heart pumps blood. Having high blood pressure can damage your heart and lead to other health problems such as heart disease and stroke.', tips: [{ icon: 'walk', text: 'Aerobic exercise lowers the blood pressure by strengthening the heart and the blood vessels.' }, { icon: 'smile', text: 'Try a relaxation technique, such as deep breathing or meditation.' }] }] }
712
+ ]
713
+
714
+ drawerOpen := false
715
+ mobile := window.matchMedia('(max-width: 1299px)').matches
716
+
717
+ setupResize: ->
718
+ mq = window.matchMedia('(max-width: 1299px)')
719
+ handler = (e) ->
720
+ mobile = e.matches
721
+ drawerOpen = false
722
+ mq.addEventListener 'change', handler
723
+
724
+ ~>
725
+ @setupResize()
726
+ window.app = { history, ranges, firstName, lastName, age, gender }
727
+
728
+ render
729
+ .app
730
+ if mobile
731
+ .header
732
+ .header__toggle
733
+ @click: -> drawerOpen = true
734
+ span "☰"
735
+ .header__title appTitle
736
+ div style: 'width: 48px'
737
+ div
738
+ class: "drawer-overlay #{if drawerOpen then 'open' else ''}"
739
+ @click: -> drawerOpen = false
740
+ div
741
+ class: "drawer #{if drawerOpen then 'open' else ''}"
742
+ Settings
743
+ firstName: firstName, lastName: lastName, age: age, gender: gender
744
+ history: history, ranges: ranges
745
+
746
+ .sidebar
747
+ .sidebar__heading appTitle
748
+ Settings
749
+ firstName: firstName, lastName: lastName, age: age, gender: gender
750
+ history: history, ranges: ranges
751
+
752
+ .preview
753
+ Brochure
754
+ title: title, poweredBy: poweredBy, lab: lab
755
+ firstName: firstName, lastName: lastName, age: age, gender: gender
756
+ history: history, ranges: ranges, topics: topics
757
+ acceptable: (k, v) -> @acceptable(k, v)
758
+ </script>
759
+
760
+ <script type="text/rip" data-name="settings">
761
+ # Settings - Form panel with two-way bound inputs
762
+
763
+ export Settings = component
764
+ @firstName := ''
765
+ @lastName := ''
766
+ @age := 0
767
+ @gender := 'Male'
768
+ @history := []
769
+ @ranges := {}
770
+
771
+ render
772
+ div
773
+ .settings__section
774
+ .settings__heading "Patient Info"
775
+ .settings__fields
776
+ .settings__field
777
+ label "First name"
778
+ input type: 'text', value <=> firstName
779
+ .settings__field
780
+ label "Last name"
781
+ input type: 'text', value <=> lastName
782
+ .settings__field
783
+ label "Age"
784
+ input type: 'number', value <=> age
785
+ .settings__field
786
+ label "Gender"
787
+ select
788
+ @change: (e) -> gender = e.target.value
789
+ option value: 'Male', selected: gender is 'Male', "Male"
790
+ option value: 'Female', selected: gender is 'Female', "Female"
791
+
792
+ .settings__section
793
+ .settings__heading "Heart Health"
794
+ .settings__fields
795
+ .settings__field
796
+ label "Triglycerides (mg/dL)"
797
+ input type: 'number', value <=> history[0].triglycerides
798
+ .settings__field
799
+ label "HDL Cholesterol (mg/dL)"
800
+ input type: 'number', value <=> history[0].hdlCholesterol
801
+ .settings__field
802
+ label "Total Cholesterol (mg/dL)"
803
+ input type: 'number', value <=> history[0].totalCholesterol
804
+ .settings__field
805
+ label "Cholesterol/HDL Ratio"
806
+ input type: 'number', step: '0.1', value <=> history[0].cholesterolHdlRatio
807
+
808
+ .settings__section
809
+ .settings__heading "Pancreas Health"
810
+ .settings__fields
811
+ .settings__field
812
+ label "Glucose (mg/dL)"
813
+ input type: 'number', value <=> history[0].glucose
814
+ .settings__field
815
+ label "Hemoglobin A1c (%)"
816
+ input type: 'number', step: '0.1', value <=> history[0].a1c
817
+
818
+ button.btn-primary "Print PDF"
819
+ @click: -> window.print()
820
+ </script>
821
+
822
+ <script type="text/rip" data-name="brochure">
823
+ # Brochure — Container composing all pages
824
+
825
+ export Brochure = component
826
+ @title := ''
827
+ @poweredBy := ''
828
+ @lab := ''
829
+ @firstName := ''
830
+ @lastName := ''
831
+ @age := 0
832
+ @gender := 'Male'
833
+ @history := []
834
+ @ranges := {}
835
+ @topics := []
836
+ @acceptable := null
837
+
838
+ render
839
+ .brochure
840
+ Cover title: title, poweredBy: poweredBy, firstName: firstName
841
+
842
+ Summary
843
+ firstName: firstName, lastName: lastName, age: age, gender: gender
844
+ lab: lab, history: history, ranges: ranges, acceptable: acceptable
845
+
846
+ for topic in topics
847
+ DetailPage topic: topic, history: history, ranges: ranges, acceptable: acceptable
848
+ </script>
849
+
850
+ <script type="text/rip" data-name="cover">
851
+ # Cover — Brochure cover page
852
+
853
+ export Cover = component
854
+ @title := ''
855
+ @poweredBy := ''
856
+ @firstName := ''
857
+
858
+ today ~=
859
+ d = new Date()
860
+ months = ['January','February','March','April','May','June','July','August','September','October','November','December']
861
+ day = d.getDate()
862
+ suffix = 'th'
863
+ suffix = 'st' if day is 1 or day is 21 or day is 31
864
+ suffix = 'nd' if day is 2 or day is 22
865
+ suffix = 'rd' if day is 3 or day is 23
866
+ "#{months[d.getMonth()]} #{day}#{suffix}, #{d.getFullYear()}"
867
+
868
+ greeting ~=
869
+ name = firstName or '(First Name)'
870
+ "#{name}, here are the results and personalized action plan from your recent screening."
871
+
872
+ render
873
+ .('brochure__page page-cover') style: "background-image: url(images/cover_bg.jpg)"
874
+ .cover__heading-fade
875
+ .cover__title title
876
+
877
+ .cover__body
878
+ .cover__copy
879
+ span greeting
880
+ br
881
+ br
882
+ span today
883
+
884
+ .cover__footer
885
+ img src: 'images/crossover.svg', alt: 'Crossover Health'
886
+ .cover__powered-by
887
+ div "Powered by"
888
+ div poweredBy
889
+ </script>
890
+
891
+ <script type="text/rip" data-name="summary">
892
+ # Summary - Medical Summary table (Page 1)
893
+
894
+ export Summary = component
895
+ @firstName := ''
896
+ @lastName := ''
897
+ @age := 0
898
+ @gender := 'Male'
899
+ @lab := ''
900
+ @history := []
901
+ @ranges := {}
902
+ @acceptable := null
903
+
904
+ patientName ~=
905
+ f = firstName or '(First Name)'
906
+ l = lastName or '(Last Name)'
907
+ "#{f} #{l}"
908
+
909
+ formattedDate ~=
910
+ d = new Date()
911
+ mo = String(d.getMonth() + 1).padStart(2, '0')
912
+ dy = String(d.getDate()).padStart(2, '0')
913
+ yr = d.getFullYear()
914
+ "#{mo}/#{dy}/#{yr}"
915
+
916
+ historyDate ~=
917
+ return '' unless history and history[0] and history[0].date
918
+ parts = history[0].date.split('-')
919
+ "#{parts[1]}/#{parts[0]}"
920
+
921
+ checkIcon := '<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/></svg>'
922
+ warnIcon := '<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"/></svg>'
923
+
924
+ sections ~=
925
+ s = []
926
+ s.push { label: 'Heart Health', keys: ['triglycerides', 'hdlCholesterol', 'totalCholesterol', 'cholesterolHdlRatio'] }
927
+ s.push { label: 'Pancreas Health', keys: ['glucose', 'a1c'] }
928
+ s.push { label: 'Physical Measurements', keys: ['waistCircumference', 'bloodPressure'] } if history?[0]?.waistCircumference
929
+ s
930
+
931
+ rowClass: (k) ->
932
+ val = history[0]?[k]
933
+ return '' unless val?
934
+ if acceptable(k, val) is 'acceptable' then 'row-acceptable' else 'row-unacceptable'
935
+
936
+ rowIcon: (k) ->
937
+ val = history[0]?[k]
938
+ return '' unless val?
939
+ if acceptable(k, val) is 'acceptable' then checkIcon else warnIcon
940
+
941
+ render
942
+ .('brochure__page page-summary')
943
+ .page-heading "Medical Summary"
944
+
945
+ .summary__intro
946
+ .summary__patient
947
+ .summary__patient-name patientName
948
+ .summary__patient-meta "#{age ? age : '(age)'} years old | #{gender}"
949
+ .summary__patient-lab
950
+ div
951
+ strong "Screening results from: "
952
+ span formattedDate
953
+ div
954
+ strong "Testing facility: "
955
+ span lab
956
+
957
+ .summary__copy "This report serves as an easy reference to review all of your testing results, including data from previous years. We encourage you to use this information in conjunction with an exam by your doctor, not as a replacement for one. We hope this summary will be a good starting point for conversations with your doctor about improving your overall health."
958
+
959
+ for section, i in sections
960
+ table.summary-table
961
+ unless i
962
+ thead
963
+ tr
964
+ th
965
+ th historyDate
966
+ tbody
967
+ tr
968
+ td section.label
969
+ td
970
+ for k in section.keys
971
+ tr
972
+ class: @rowClass(k)
973
+ td
974
+ .status-icon
975
+ innerHTML: @rowIcon(k)
976
+ div
977
+ .name ranges[k].name
978
+ .desc ranges[k].desc
979
+ td.value history[0]?[k]
980
+ </script>
981
+
982
+ <script type="text/rip" data-name="detail-page">
983
+ # DetailPage - Reusable health detail page with gradient header and gauges
984
+
985
+ export DetailPage = component
986
+ @topic := {}
987
+ @history := []
988
+ @ranges := {}
989
+ @acceptable := null
990
+
991
+ shouldShow ~=
992
+ return true unless topic.conditional
993
+ history?[0]?[topic.conditional]?
994
+
995
+ tipIcons :=
996
+ utensils: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M3 2v7c0 1.1.9 2 2 2h4c1.1 0 2-.9 2-2V2"/><line x1="7" y1="2" x2="7" y2="22"/><path d="M21 15V2a5 5 0 00-5 5v6h4"/><line x1="21" y1="12" x2="21" y2="22"/></svg>'
997
+ drop: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M12 2.69l5.66 5.66a8 8 0 11-11.31 0z"/></svg>'
998
+ walk: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="13" cy="4" r="2"/><path d="M7 21l3-4V11l-2-2-3 3"/><path d="M16 21l-2-6-3-1"/></svg>'
999
+ doctor: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M12 11V3M4.93 7.5l7.07 3.5 7.07-3.5"/><circle cx="12" cy="18" r="3"/><line x1="12" y1="15" x2="12" y2="11"/></svg>'
1000
+ smile: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="12" cy="12" r="10"/><path d="M8 14s1.5 2 4 2 4-2 4-2"/><line x1="9" y1="9" x2="9.01" y2="9"/><line x1="15" y1="9" x2="15.01" y2="9"/></svg>'
1001
+
1002
+ checkIcon := '<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/></svg>'
1003
+ warnIcon := '<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"/></svg>'
1004
+
1005
+ statusOf: (key) ->
1006
+ val = history[0]?[key]
1007
+ return 'acceptable' unless val?
1008
+ acceptable(key, val)
1009
+
1010
+ iconFor: (key) ->
1011
+ if @statusOf(key) is 'acceptable' then checkIcon else warnIcon
1012
+
1013
+ render
1014
+ if shouldShow
1015
+ .('brochure__page page-detail')
1016
+ if topic.gradient
1017
+ .detail__header style: "background: #{topic.gradient}"
1018
+ .detail__header-image style: "background-image: url(#{topic.image}); width: #{topic.imageWidth}px; height: #{topic.imageHeight}px"
1019
+ .detail__header-text
1020
+ .detail__header-title topic.title
1021
+ .detail__header-copy topic.copy
1022
+
1023
+ .detail__content
1024
+ for bio in topic.biomarkers
1025
+ .detail__item
1026
+ if ranges[bio.key]
1027
+ Gauge value: (history[0]?[bio.key] or 0), units: ranges[bio.key].units, range: ranges[bio.key].range
1028
+
1029
+ .detail__item-header class: @statusOf(bio.key)
1030
+ .status-icon
1031
+ innerHTML: @iconFor(bio.key)
1032
+ div
1033
+ .name ranges[bio.key].name
1034
+ .desc ranges[bio.key].desc
1035
+
1036
+ .detail__item-info
1037
+ p ranges[bio.key].info[@statusOf(bio.key)]
1038
+ p innerHTML: bio.text
1039
+ if bio.extra
1040
+ p bio.extra
1041
+
1042
+ .detail__maintenance
1043
+ .detail__maintenance-heading "How To Maintain"
1044
+ for tip in bio.tips
1045
+ .detail__maintenance-item
1046
+ .detail__maintenance-icon
1047
+ innerHTML: (tipIcons[tip.icon] or '')
1048
+ div tip.text
1049
+
1050
+ if topic.isImagePage
1051
+ .detail__footer-image style: "background-image: url(#{topic.image})"
1052
+ </script>
1053
+
1054
+ <script type="text/rip" data-name="gauge">
1055
+ # Gauge — SVG semi-circular gauge with rotating needle
1056
+
1057
+ export Gauge = component
1058
+ @value := 0
1059
+ @units := ''
1060
+ @range := []
1061
+
1062
+ gaugeType ~=
1063
+ switch range[0]
1064
+ when '<', '<=' then 'lessThan'
1065
+ when '>', '>=' then 'greaterThan'
1066
+ when '<>' then 'inBetween'
1067
+ else null
1068
+
1069
+ needleRotation ~=
1070
+ max = 75
1071
+ fraction = 0
1072
+
1073
+ if typeof range[1] is 'string' and String(range[1]).includes('/')
1074
+ bp = String(range[1]).split('/')
1075
+ bv = String(value).split('/')
1076
+ sf = Number(bv[0]) / (Number(bp[0]) * 2)
1077
+ df = Number(bv[1]) / (Number(bp[1]) * 2)
1078
+ fraction = (sf + df) / 2
1079
+ else
1080
+ switch gaugeType
1081
+ when 'lessThan', 'greaterThan'
1082
+ fraction = Number(value) / (Number(range[1]) * 2)
1083
+ when 'inBetween'
1084
+ fraction = Number(value) / (Number(range[1]) + Number(range[2]))
1085
+
1086
+ fraction = 1 if fraction >= 1
1087
+ fraction = 0 if fraction <= 0
1088
+ fraction * (max * 2) - max
1089
+
1090
+ render
1091
+ .('gauge', gaugeType is 'lessThan' and 'gauge--flip')
1092
+ if gaugeType is 'lessThan' or gaugeType is 'greaterThan'
1093
+ svg width: '170', height: '85', viewBox: '0 0 170 85'
1094
+ g stroke: 'none', fill: 'none'
1095
+ g transform: 'translate(-31, -36)'
1096
+ g transform: 'translate(30, 36)'
1097
+ path d: 'M86.09,0 C132.98,0 171,38.06 171,85.01 L156.85,85.01 C156.85,45.89 125.17,14.17 86.09,14.17 L86.09,0 Z', fill: '#2EAB27'
1098
+ path d: 'M1.18,0 C48.07,0 86.09,38.06 86.09,85.01 L71.94,85.01 C71.94,45.89 40.26,14.17 1.18,14.17 L1.18,0 Z', fill: '#FB2E47', transform: 'translate(43.63, 42.51) scale(-1, 1) translate(-43.63, -42.51)'
1099
+ g transform: 'translate(20.23, 19.25)'
1100
+ path d: 'M0.77,65.75 C0.77,29.85 30.1,0.75 66.27,0.75 C102.45,0.75 131.77,29.85 131.77,65.75 L0.77,65.75 Z', fill: '#2EAB27'
1101
+ path d: 'M8.77,65.75 C8.77,34.27 34.52,8.75 66.27,8.75 C98.03,8.75 123.77,34.27 123.77,65.75 L8.77,65.75 Z', fill: '#20781C'
1102
+ else
1103
+ svg width: '170', height: '85', viewBox: '0 0 170 85'
1104
+ g stroke: 'none', fill: 'none'
1105
+ g transform: 'translate(-31, -36)'
1106
+ g transform: 'translate(30, 0)'
1107
+ path d: 'M43.6,18.43 C90.49,18.43 128.51,56.49 128.51,103.44 L114.36,103.44 C114.36,64.32 82.68,32.6 43.6,32.6 L43.6,18.43 Z', fill: '#2EAB27', transform: 'translate(86.05, 60.94) rotate(-45) translate(-86.05, -60.94)'
1108
+ path d: 'M15.29,121 L1.13,121 C1.11,99.24 9.39,77.49 25.98,60.9 L36,70.92 C22.17,84.74 15.27,102.87 15.29,121 Z', fill: '#FB2E47'
1109
+ path d: 'M170.96,121 L156.8,121 C156.79,102.9 149.89,84.82 136.09,71.02 L146.11,61 C162.68,77.56 170.96,99.28 170.96,121 Z', fill: '#FB2E47'
1110
+ g transform: 'translate(20.23, 55.25)'
1111
+ path d: 'M0.77,65.75 C0.77,29.85 30.1,0.75 66.27,0.75 C102.45,0.75 131.77,29.85 131.77,65.75 L0.77,65.75 Z', fill: '#2EAB27'
1112
+ path d: 'M8.77,65.75 C8.77,34.27 34.52,8.75 66.27,8.75 C98.03,8.75 123.77,34.27 123.77,65.75 L8.77,65.75 Z', fill: '#20781C'
1113
+
1114
+ .gauge__needle style: "transform: rotate(#{needleRotation}deg)"
1115
+ .gauge__arrow
1116
+ .gauge__arrow-border
1117
+
1118
+ .gauge__value "#{value}"
1119
+ .gauge__units units
1120
+ </script>
1121
+
1122
+ <!-- ===== Boot ===== -->
1123
+
1124
+ <script type="text/rip">
1125
+ { launch } = importRip! '/rip/ui.rip'
1126
+ launch hash: true
1127
+ </script>
1128
+
1129
+ </body>
1130
+ </html>