rip-lang 3.10.6 → 3.10.7

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,1117 @@
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
+ }
438
+
439
+ .detail__content {
440
+ display: flex;
441
+ padding: 25px;
442
+ flex: 1;
443
+ }
444
+
445
+ .detail__item {
446
+ width: 50%;
447
+ background: white;
448
+ border: 1px solid #f0f0f0;
449
+ border-radius: var(--radius);
450
+ padding: 25px;
451
+ }
452
+
453
+ .detail__item + .detail__item { margin-left: 25px; }
454
+
455
+ .detail__item-header {
456
+ margin-top: 15px;
457
+ display: flex;
458
+ align-items: center;
459
+ gap: 12px;
460
+ }
461
+
462
+ .detail__item-header .name { font-size: 18px; font-weight: 600; }
463
+ .detail__item-header .desc { font-size: 15px; color: rgba(51,51,51,.7); }
464
+
465
+ .detail__item-header.acceptable { color: var(--color-success); }
466
+ .detail__item-header.unacceptable { color: var(--color-danger); }
467
+
468
+ .detail__item-info {
469
+ margin-top: 20px;
470
+ color: rgba(51,51,51,.9);
471
+ line-height: 1.5;
472
+ }
473
+
474
+ .detail__item-info p { margin: 1rem 0; }
475
+ .detail__item-info p:first-child { margin-top: 0; }
476
+ .detail__item-info strong { font-weight: 700; }
477
+
478
+ .detail__maintenance { margin-top: 25px; }
479
+
480
+ .detail__maintenance-heading {
481
+ text-transform: uppercase;
482
+ font-weight: 600;
483
+ font-size: 14px;
484
+ }
485
+
486
+ .detail__maintenance-item {
487
+ margin-top: 15px;
488
+ display: flex;
489
+ align-items: baseline;
490
+ gap: 12px;
491
+ color: rgba(51,51,51,.9);
492
+ line-height: 1.4;
493
+ }
494
+
495
+ .detail__maintenance-icon {
496
+ width: 16px;
497
+ flex-shrink: 0;
498
+ display: flex;
499
+ justify-content: center;
500
+ color: rgba(51,51,51,.5);
501
+ }
502
+
503
+ .gauge {
504
+ position: relative;
505
+ display: inline-block;
506
+ }
507
+
508
+ .gauge--flip svg { transform: scaleX(-1); }
509
+
510
+ .gauge__needle {
511
+ position: absolute;
512
+ width: 131px;
513
+ height: 131px;
514
+ margin: 0 auto;
515
+ border-radius: 50%;
516
+ top: 21px;
517
+ left: 0;
518
+ right: 0;
519
+ }
520
+
521
+ .gauge__arrow,
522
+ .gauge__arrow-border {
523
+ position: absolute;
524
+ margin: 0 auto;
525
+ left: 0; right: 0;
526
+ width: 0; height: 0;
527
+ }
528
+
529
+ .gauge__arrow {
530
+ top: -13px;
531
+ z-index: 2;
532
+ border-left: 15px solid transparent;
533
+ border-right: 15px solid transparent;
534
+ border-bottom: 15px solid var(--color-success);
535
+ }
536
+
537
+ .gauge__arrow-border {
538
+ top: -15px;
539
+ z-index: 1;
540
+ border-left: 15px solid transparent;
541
+ border-right: 15px solid transparent;
542
+ border-bottom: 15px solid white;
543
+ }
544
+
545
+ .gauge__value,
546
+ .gauge__units {
547
+ font-weight: 600;
548
+ position: absolute;
549
+ left: 0; right: 0;
550
+ text-align: center;
551
+ }
552
+
553
+ .gauge__value { font-size: 18px; color: white; bottom: 26px; }
554
+ .gauge__units { font-size: 13px; color: rgba(255,255,255,.9); bottom: 13px; }
555
+
556
+ @media print {
557
+ body { background: white; }
558
+
559
+ .header, .sidebar, .drawer, .drawer-overlay, .btn-primary,
560
+ .settings__section { display: none !important; }
561
+
562
+ .app { padding: 0; display: block; }
563
+ .preview { padding: 0; margin: 0; }
564
+
565
+ .brochure { width: 100%; box-shadow: none; }
566
+
567
+ .brochure__page {
568
+ width: 100%;
569
+ height: 100%;
570
+ position: absolute;
571
+ top: 0; left: 0;
572
+ margin: auto;
573
+ border: none;
574
+ page-break-before: always;
575
+ }
576
+
577
+ .brochure__page:first-child { page-break-before: auto; }
578
+ }
579
+ </style>
580
+ </head>
581
+ <body>
582
+
583
+ <!-- ===== Components ===== -->
584
+
585
+ <script type="text/rip" data-name="index">
586
+ # Lab Results - Main Page
587
+
588
+ export Home = component
589
+ appTitle := 'Lab Results'
590
+ title := 'My Guide to Health'
591
+ poweredBy := 'Crossover Health'
592
+ lab := 'Medical Diagnostics'
593
+
594
+ firstName := 'Scott'
595
+ lastName := 'Bowman'
596
+ age := 42
597
+ gender := 'Male'
598
+
599
+ history := [
600
+ date: '2022-10'
601
+ triglycerides: 181
602
+ hdlCholesterol: 44
603
+ totalCholesterol: 177
604
+ cholesterolHdlRatio: 4.3
605
+ glucose: 75
606
+ a1c: 5.2
607
+ ]
608
+
609
+ ranges :=
610
+ triglycerides:
611
+ name: 'Triglycerides'
612
+ range: ['<', 150]
613
+ units: 'mg/dL'
614
+ desc: 'Reference range: < 150 mg/dL'
615
+ info:
616
+ acceptable: 'Your result falls within the normal Reference Range.'
617
+ unacceptable: 'Your result falls outside the normal Reference Range.'
618
+ hdlCholesterol:
619
+ name: 'HDL Cholesterol'
620
+ range: ['>=', 40]
621
+ units: 'mg/dL'
622
+ desc: 'Reference range: ≥ 40 mg/dL'
623
+ info:
624
+ acceptable: 'Your result falls within the normal Reference Range.'
625
+ unacceptable: 'Your result falls outside the normal Reference Range.'
626
+ totalCholesterol:
627
+ name: 'Total Cholesterol'
628
+ range: ['<>', 125, 199]
629
+ units: 'mg/dL'
630
+ desc: 'Reference range: 125-199 mg/dL'
631
+ info:
632
+ acceptable: 'Your result falls within the normal Reference Range.'
633
+ unacceptable: 'Your result falls outside the normal Reference Range.'
634
+ cholesterolHdlRatio:
635
+ name: 'Cholesterol / HDL Ratio'
636
+ range: ['<=', 5]
637
+ units: '(calc)'
638
+ desc: 'Reference range: ≤ 5.0 (calc)'
639
+ info:
640
+ acceptable: 'Your result falls within the normal Reference Range.'
641
+ unacceptable: 'Your result falls outside the normal Reference Range.'
642
+ glucose:
643
+ name: 'Glucose'
644
+ range: ['<>', 65, 99]
645
+ units: 'mg/dL'
646
+ desc: 'Reference range: 65-99 mg/dL'
647
+ info:
648
+ acceptable: 'Your result falls within the normal Reference Range.'
649
+ unacceptable: 'Your result falls outside the normal Reference Range.'
650
+ a1c:
651
+ name: 'Hemoglobin A1c'
652
+ range: ['<', 5.7]
653
+ units: '%'
654
+ desc: 'Reference range: < 5.7%'
655
+ info:
656
+ acceptable: 'Your result falls within the normal Reference Range.'
657
+ unacceptable: 'Your result falls outside the normal Reference Range.'
658
+ waistCircumference:
659
+ name: 'Waist Circumference'
660
+ range: ['<=', 40]
661
+ units: 'in'
662
+ desc: 'Reference range: ≤ 40 in'
663
+ info:
664
+ acceptable: 'Your result falls within the normal Reference Range.'
665
+ unacceptable: 'Your result falls outside the normal Reference Range.'
666
+ bloodPressure:
667
+ name: 'Blood Pressure'
668
+ range: ['<', '120/80']
669
+ units: 'mm/Hg'
670
+ desc: 'Reference range: < 120/80 mmHg'
671
+ info:
672
+ acceptable: 'Your result falls within the normal Reference Range.'
673
+ unacceptable: 'Your result falls outside the normal Reference Range.'
674
+
675
+ acceptable: (key, value) ->
676
+ ref = ranges[key]
677
+ return 'acceptable' unless ref
678
+ r = ref.range
679
+ ok = false
680
+ if typeof r[1] is 'string' and r[1].includes('/')
681
+ bp = r[1].split('/')
682
+ bv = String(value).split('/')
683
+ ok = Number(bv[0]) <= Number(bp[0]) and Number(bv[1]) <= Number(bp[1])
684
+ else
685
+ switch r[0]
686
+ when '<' then ok = value < r[1]
687
+ when '<=' then ok = value <= r[1]
688
+ when '>=' then ok = value >= r[1]
689
+ when '<>' then ok = value >= r[1] and value <= r[2]
690
+ if ok then 'acceptable' else 'unacceptable'
691
+
692
+ topics := [
693
+ { 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.' }] }] }
694
+ { 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.' }] }] }
695
+ { 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.' }] }] }
696
+ { 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.' }] }] }
697
+ ]
698
+
699
+ drawerOpen := false
700
+ mobile := window.matchMedia('(max-width: 1299px)').matches
701
+
702
+ setupResize: ->
703
+ mq = window.matchMedia('(max-width: 1299px)')
704
+ handler = (e) ->
705
+ mobile = e.matches
706
+ drawerOpen = false
707
+ mq.addEventListener 'change', handler
708
+
709
+ ~> @setupResize()
710
+
711
+ render
712
+ .app
713
+ if mobile
714
+ .header
715
+ .header__toggle
716
+ @click: -> drawerOpen = true
717
+ span "☰"
718
+ .header__title appTitle
719
+ div style: 'width: 48px'
720
+ div
721
+ class: "drawer-overlay #{if drawerOpen then 'open' else ''}"
722
+ @click: -> drawerOpen = false
723
+ div
724
+ class: "drawer #{if drawerOpen then 'open' else ''}"
725
+ Settings
726
+ firstName: firstName, lastName: lastName, age: age, gender: gender
727
+ history: history, ranges: ranges
728
+
729
+ .sidebar
730
+ .sidebar__heading appTitle
731
+ Settings
732
+ firstName: firstName, lastName: lastName, age: age, gender: gender
733
+ history: history, ranges: ranges
734
+
735
+ .preview
736
+ Brochure
737
+ title: title, poweredBy: poweredBy, lab: lab
738
+ firstName: firstName, lastName: lastName, age: age, gender: gender
739
+ history: history, ranges: ranges, topics: topics
740
+ acceptable: (k, v) -> @acceptable(k, v)
741
+ </script>
742
+
743
+ <script type="text/rip" data-name="settings">
744
+ # Settings - Form panel with two-way bound inputs
745
+
746
+ export Settings = component
747
+ @firstName := ''
748
+ @lastName := ''
749
+ @age := 0
750
+ @gender := 'Male'
751
+ @history := []
752
+ @ranges := {}
753
+
754
+ render
755
+ div
756
+ .settings__section
757
+ .settings__heading "Patient Info"
758
+ .settings__fields
759
+ .settings__field
760
+ label "First name"
761
+ input type: 'text', value <=> firstName
762
+ .settings__field
763
+ label "Last name"
764
+ input type: 'text', value <=> lastName
765
+ .settings__field
766
+ label "Age"
767
+ input type: 'number', value <=> age
768
+ .settings__field
769
+ label "Gender"
770
+ select
771
+ @change: (e) -> gender = e.target.value
772
+ option value: 'Male', selected: gender is 'Male', "Male"
773
+ option value: 'Female', selected: gender is 'Female', "Female"
774
+
775
+ .settings__section
776
+ .settings__heading "Heart Health"
777
+ .settings__fields
778
+ .settings__field
779
+ label "Triglycerides (mg/dL)"
780
+ input type: 'number', value <=> history[0].triglycerides
781
+ .settings__field
782
+ label "HDL Cholesterol (mg/dL)"
783
+ input type: 'number', value <=> history[0].hdlCholesterol
784
+ .settings__field
785
+ label "Total Cholesterol (mg/dL)"
786
+ input type: 'number', value <=> history[0].totalCholesterol
787
+ .settings__field
788
+ label "Cholesterol/HDL Ratio"
789
+ input type: 'number', step: '0.1', value <=> history[0].cholesterolHdlRatio
790
+
791
+ .settings__section
792
+ .settings__heading "Pancreas Health"
793
+ .settings__fields
794
+ .settings__field
795
+ label "Glucose (mg/dL)"
796
+ input type: 'number', value <=> history[0].glucose
797
+ .settings__field
798
+ label "Hemoglobin A1c (%)"
799
+ input type: 'number', step: '0.1', value <=> history[0].a1c
800
+
801
+ button.btn-primary "Print PDF"
802
+ @click: -> window.print()
803
+ </script>
804
+
805
+ <script type="text/rip" data-name="brochure">
806
+ # Brochure — Container composing all pages
807
+
808
+ export Brochure = component
809
+ @title := ''
810
+ @poweredBy := ''
811
+ @lab := ''
812
+ @firstName := ''
813
+ @lastName := ''
814
+ @age := 0
815
+ @gender := 'Male'
816
+ @history := []
817
+ @ranges := {}
818
+ @topics := []
819
+ @acceptable := null
820
+
821
+ render
822
+ .brochure
823
+ Cover title: title, poweredBy: poweredBy, firstName: firstName
824
+
825
+ Summary
826
+ firstName: firstName, lastName: lastName, age: age, gender: gender
827
+ lab: lab, history: history, ranges: ranges, acceptable: acceptable
828
+
829
+ for topic in topics
830
+ DetailPage topic: topic, history: history, ranges: ranges, acceptable: acceptable
831
+ </script>
832
+
833
+ <script type="text/rip" data-name="cover">
834
+ # Cover — Brochure cover page
835
+
836
+ export Cover = component
837
+ @title := ''
838
+ @poweredBy := ''
839
+ @firstName := ''
840
+
841
+ today ~=
842
+ d = new Date()
843
+ months = ['January','February','March','April','May','June','July','August','September','October','November','December']
844
+ day = d.getDate()
845
+ suffix = 'th'
846
+ suffix = 'st' if day is 1 or day is 21 or day is 31
847
+ suffix = 'nd' if day is 2 or day is 22
848
+ suffix = 'rd' if day is 3 or day is 23
849
+ "#{months[d.getMonth()]} #{day}#{suffix}, #{d.getFullYear()}"
850
+
851
+ greeting ~=
852
+ name = firstName or '(First Name)'
853
+ "#{name}, here are the results and personalized action plan from your recent screening."
854
+
855
+ render
856
+ .('brochure__page page-cover') style: "background-image: url(images/cover_bg.jpg)"
857
+ .cover__heading-fade
858
+ .cover__title title
859
+
860
+ .cover__body
861
+ .cover__copy
862
+ span greeting
863
+ br
864
+ br
865
+ span today
866
+
867
+ .cover__footer
868
+ img src: 'images/crossover.svg', alt: 'Crossover Health'
869
+ .cover__powered-by
870
+ div "Powered by"
871
+ div poweredBy
872
+ </script>
873
+
874
+ <script type="text/rip" data-name="summary">
875
+ # Summary - Medical Summary table (Page 1)
876
+
877
+ export Summary = component
878
+ @firstName := ''
879
+ @lastName := ''
880
+ @age := 0
881
+ @gender := 'Male'
882
+ @lab := ''
883
+ @history := []
884
+ @ranges := {}
885
+ @acceptable := null
886
+
887
+ patientName ~=
888
+ f = firstName or '(First Name)'
889
+ l = lastName or '(Last Name)'
890
+ "#{f} #{l}"
891
+
892
+ formattedDate ~=
893
+ d = new Date()
894
+ mo = String(d.getMonth() + 1).padStart(2, '0')
895
+ dy = String(d.getDate()).padStart(2, '0')
896
+ yr = d.getFullYear()
897
+ "#{mo}/#{dy}/#{yr}"
898
+
899
+ historyDate ~=
900
+ return '' unless history and history[0] and history[0].date
901
+ parts = history[0].date.split('-')
902
+ "#{parts[1]}/#{parts[0]}"
903
+
904
+ 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>'
905
+ 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>'
906
+
907
+ sections ~=
908
+ s = []
909
+ s.push { label: 'Heart Health', keys: ['triglycerides', 'hdlCholesterol', 'totalCholesterol', 'cholesterolHdlRatio'] }
910
+ s.push { label: 'Pancreas Health', keys: ['glucose', 'a1c'] }
911
+ if history and history[0] and history[0].waistCircumference?
912
+ s.push { label: 'Physical Measurements', keys: ['waistCircumference', 'bloodPressure'] }
913
+ s
914
+
915
+ rows ~=
916
+ result = []
917
+ for section in sections
918
+ for k in section.keys
919
+ ref = ranges[k]
920
+ continue unless ref
921
+ val = history[0]?[k]
922
+ continue unless val?
923
+ status = acceptable(k, val)
924
+ result.push
925
+ section: section.label
926
+ key: k
927
+ name: ref.name
928
+ desc: ref.desc
929
+ value: String(val)
930
+ rowClass: if status is 'acceptable' then 'row-acceptable' else 'row-unacceptable'
931
+ icon: if status is 'acceptable' then checkIcon else warnIcon
932
+ result
933
+
934
+ render
935
+ .('brochure__page page-summary')
936
+ .page-heading "Medical Summary"
937
+
938
+ .summary__intro
939
+ .summary__patient
940
+ .summary__patient-name patientName
941
+ .summary__patient-meta "#{age ? age : '(age)'} years old | #{gender}"
942
+ .summary__patient-lab
943
+ div
944
+ strong "Screening results from: "
945
+ span formattedDate
946
+ div
947
+ strong "Testing facility: "
948
+ span lab
949
+
950
+ .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."
951
+
952
+ table.summary-table
953
+ thead
954
+ tr
955
+ th ""
956
+ th historyDate
957
+ tbody
958
+ for row in rows
959
+ tr
960
+ class: row.rowClass
961
+ td
962
+ .status-icon
963
+ innerHTML: row.icon
964
+ div
965
+ .name row.name
966
+ .desc row.desc
967
+ td.value row.value
968
+ </script>
969
+
970
+ <script type="text/rip" data-name="detail-page">
971
+ # DetailPage - Reusable health detail page with gradient header and gauges
972
+
973
+ export DetailPage = component
974
+ @topic := {}
975
+ @history := []
976
+ @ranges := {}
977
+ @acceptable := null
978
+
979
+ shouldShow ~=
980
+ return true unless topic.conditional
981
+ history and history[0] and history[0][topic.conditional]?
982
+
983
+ tipIcons :=
984
+ 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>'
985
+ 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>'
986
+ 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>'
987
+ 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>'
988
+ 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>'
989
+
990
+ 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>'
991
+ 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>'
992
+
993
+ statusOf: (key) ->
994
+ val = history[0]?[key]
995
+ return 'acceptable' unless val?
996
+ acceptable(key, val)
997
+
998
+ iconFor: (key) ->
999
+ if @statusOf(key) is 'acceptable' then checkIcon else warnIcon
1000
+
1001
+ render
1002
+ if shouldShow
1003
+ .('brochure__page page-detail')
1004
+ if topic.gradient
1005
+ .detail__header style: "background: #{topic.gradient}"
1006
+ .detail__header-image style: "background-image: url(#{topic.image}); width: #{topic.imageWidth}px; height: #{topic.imageHeight}px"
1007
+ .detail__header-text
1008
+ .detail__header-title topic.title
1009
+ .detail__header-copy topic.copy
1010
+
1011
+ if topic.isImagePage
1012
+ .detail__header style: "background-image: url(#{topic.image}); background-size: cover"
1013
+
1014
+ .detail__content
1015
+ for bio in topic.biomarkers
1016
+ .detail__item
1017
+ if ranges[bio.key]
1018
+ Gauge value: (history[0]?[bio.key] or 0), units: ranges[bio.key].units, range: ranges[bio.key].range
1019
+
1020
+ .detail__item-header class: @statusOf(bio.key)
1021
+ .status-icon
1022
+ innerHTML: @iconFor(bio.key)
1023
+ div
1024
+ .name ranges[bio.key].name
1025
+ .desc ranges[bio.key].desc
1026
+
1027
+ .detail__item-info
1028
+ p innerHTML: bio.text
1029
+ if bio.extra
1030
+ p bio.extra
1031
+
1032
+ .detail__maintenance
1033
+ .detail__maintenance-heading "How To Maintain"
1034
+ for tip in bio.tips
1035
+ .detail__maintenance-item
1036
+ .detail__maintenance-icon
1037
+ innerHTML: (tipIcons[tip.icon] or '')
1038
+ div tip.text
1039
+ </script>
1040
+
1041
+ <script type="text/rip" data-name="gauge">
1042
+ # Gauge — SVG semi-circular gauge with rotating needle
1043
+
1044
+ export Gauge = component
1045
+ @value := 0
1046
+ @units := ''
1047
+ @range := []
1048
+
1049
+ gaugeType ~=
1050
+ switch range[0]
1051
+ when '<', '<=' then 'lessThan'
1052
+ when '>', '>=' then 'greaterThan'
1053
+ when '<>' then 'inBetween'
1054
+ else null
1055
+
1056
+ needleRotation ~=
1057
+ max = 75
1058
+ fraction = 0
1059
+
1060
+ if typeof range[1] is 'string' and String(range[1]).includes('/')
1061
+ bp = String(range[1]).split('/')
1062
+ bv = String(value).split('/')
1063
+ sf = Number(bv[0]) / (Number(bp[0]) * 2)
1064
+ df = Number(bv[1]) / (Number(bp[1]) * 2)
1065
+ fraction = (sf + df) / 2
1066
+ else
1067
+ switch gaugeType
1068
+ when 'lessThan', 'greaterThan'
1069
+ fraction = Number(value) / (Number(range[1]) * 2)
1070
+ when 'inBetween'
1071
+ fraction = Number(value) / (Number(range[1]) + Number(range[2]))
1072
+
1073
+ fraction = 1 if fraction >= 1
1074
+ fraction = 0 if fraction <= 0
1075
+ fraction * (max * 2) - max
1076
+
1077
+ render
1078
+ .('gauge', gaugeType is 'lessThan' and 'gauge--flip')
1079
+ if gaugeType is 'lessThan' or gaugeType is 'greaterThan'
1080
+ svg width: '170', height: '85', viewBox: '0 0 170 85'
1081
+ g stroke: 'none', fill: 'none'
1082
+ g transform: 'translate(-31, -36)'
1083
+ g transform: 'translate(30, 36)'
1084
+ 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'
1085
+ 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)'
1086
+ g transform: 'translate(20.23, 19.25)'
1087
+ 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'
1088
+ 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'
1089
+ else
1090
+ svg width: '170', height: '85', viewBox: '0 0 170 85'
1091
+ g stroke: 'none', fill: 'none'
1092
+ g transform: 'translate(-31, -36)'
1093
+ g transform: 'translate(30, 0)'
1094
+ 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)'
1095
+ 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'
1096
+ 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'
1097
+ g transform: 'translate(20.23, 55.25)'
1098
+ 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'
1099
+ 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'
1100
+
1101
+ .gauge__needle style: "transform: rotate(#{needleRotation}deg)"
1102
+ .gauge__arrow
1103
+ .gauge__arrow-border
1104
+
1105
+ .gauge__value "#{value}"
1106
+ .gauge__units units
1107
+ </script>
1108
+
1109
+ <!-- ===== Boot ===== -->
1110
+
1111
+ <script type="text/rip">
1112
+ { launch } = importRip! '/rip/ui.rip'
1113
+ launch hash: true
1114
+ </script>
1115
+
1116
+ </body>
1117
+ </html>