@jjlmoya/utils-health 1.35.0 → 1.37.0

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.
Files changed (80) hide show
  1. package/package.json +1 -1
  2. package/src/category/index.ts +4 -1
  3. package/src/entries.ts +7 -1
  4. package/src/index.ts +3 -0
  5. package/src/tests/locale_completeness.test.ts +1 -1
  6. package/src/tests/tool_validation.test.ts +2 -2
  7. package/src/tool/correctedCalciumCalculator/bibliography.astro +14 -0
  8. package/src/tool/correctedCalciumCalculator/bibliography.ts +20 -0
  9. package/src/tool/correctedCalciumCalculator/component.astro +317 -0
  10. package/src/tool/correctedCalciumCalculator/corrected-calcium-calculator.css +492 -0
  11. package/src/tool/correctedCalciumCalculator/entry.ts +27 -0
  12. package/src/tool/correctedCalciumCalculator/i18n/de.ts +292 -0
  13. package/src/tool/correctedCalciumCalculator/i18n/en.ts +292 -0
  14. package/src/tool/correctedCalciumCalculator/i18n/es.ts +292 -0
  15. package/src/tool/correctedCalciumCalculator/i18n/fr.ts +292 -0
  16. package/src/tool/correctedCalciumCalculator/i18n/id.ts +292 -0
  17. package/src/tool/correctedCalciumCalculator/i18n/it.ts +292 -0
  18. package/src/tool/correctedCalciumCalculator/i18n/ja.ts +292 -0
  19. package/src/tool/correctedCalciumCalculator/i18n/ko.ts +292 -0
  20. package/src/tool/correctedCalciumCalculator/i18n/nl.ts +292 -0
  21. package/src/tool/correctedCalciumCalculator/i18n/pl.ts +292 -0
  22. package/src/tool/correctedCalciumCalculator/i18n/pt.ts +292 -0
  23. package/src/tool/correctedCalciumCalculator/i18n/ru.ts +292 -0
  24. package/src/tool/correctedCalciumCalculator/i18n/sv.ts +292 -0
  25. package/src/tool/correctedCalciumCalculator/i18n/tr.ts +292 -0
  26. package/src/tool/correctedCalciumCalculator/i18n/zh.ts +292 -0
  27. package/src/tool/correctedCalciumCalculator/index.ts +12 -0
  28. package/src/tool/correctedCalciumCalculator/logic.ts +55 -0
  29. package/src/tool/correctedCalciumCalculator/seo.astro +15 -0
  30. package/src/tool/correctedCalciumCalculator/ui.ts +30 -0
  31. package/src/tool/creatinineClearanceCockcroftGault/bibliography.astro +14 -0
  32. package/src/tool/creatinineClearanceCockcroftGault/bibliography.ts +28 -0
  33. package/src/tool/creatinineClearanceCockcroftGault/client.ts +149 -0
  34. package/src/tool/creatinineClearanceCockcroftGault/component.astro +147 -0
  35. package/src/tool/creatinineClearanceCockcroftGault/creatinine-clearance-cockcroft-gault.css +550 -0
  36. package/src/tool/creatinineClearanceCockcroftGault/entry.ts +27 -0
  37. package/src/tool/creatinineClearanceCockcroftGault/i18n/de.ts +306 -0
  38. package/src/tool/creatinineClearanceCockcroftGault/i18n/en.ts +306 -0
  39. package/src/tool/creatinineClearanceCockcroftGault/i18n/es.ts +306 -0
  40. package/src/tool/creatinineClearanceCockcroftGault/i18n/fr.ts +306 -0
  41. package/src/tool/creatinineClearanceCockcroftGault/i18n/id.ts +306 -0
  42. package/src/tool/creatinineClearanceCockcroftGault/i18n/it.ts +306 -0
  43. package/src/tool/creatinineClearanceCockcroftGault/i18n/ja.ts +306 -0
  44. package/src/tool/creatinineClearanceCockcroftGault/i18n/ko.ts +306 -0
  45. package/src/tool/creatinineClearanceCockcroftGault/i18n/nl.ts +306 -0
  46. package/src/tool/creatinineClearanceCockcroftGault/i18n/pl.ts +306 -0
  47. package/src/tool/creatinineClearanceCockcroftGault/i18n/pt.ts +306 -0
  48. package/src/tool/creatinineClearanceCockcroftGault/i18n/ru.ts +306 -0
  49. package/src/tool/creatinineClearanceCockcroftGault/i18n/sv.ts +306 -0
  50. package/src/tool/creatinineClearanceCockcroftGault/i18n/tr.ts +306 -0
  51. package/src/tool/creatinineClearanceCockcroftGault/i18n/zh.ts +306 -0
  52. package/src/tool/creatinineClearanceCockcroftGault/index.ts +12 -0
  53. package/src/tool/creatinineClearanceCockcroftGault/logic.ts +99 -0
  54. package/src/tool/creatinineClearanceCockcroftGault/seo.astro +15 -0
  55. package/src/tool/creatinineClearanceCockcroftGault/ui.ts +59 -0
  56. package/src/tool/egfrCalculator/bibliography.astro +14 -0
  57. package/src/tool/egfrCalculator/bibliography.ts +20 -0
  58. package/src/tool/egfrCalculator/component.astro +298 -0
  59. package/src/tool/egfrCalculator/egfr-calculator.css +399 -0
  60. package/src/tool/egfrCalculator/entry.ts +27 -0
  61. package/src/tool/egfrCalculator/i18n/de.ts +242 -0
  62. package/src/tool/egfrCalculator/i18n/en.ts +242 -0
  63. package/src/tool/egfrCalculator/i18n/es.ts +242 -0
  64. package/src/tool/egfrCalculator/i18n/fr.ts +242 -0
  65. package/src/tool/egfrCalculator/i18n/id.ts +242 -0
  66. package/src/tool/egfrCalculator/i18n/it.ts +242 -0
  67. package/src/tool/egfrCalculator/i18n/ja.ts +242 -0
  68. package/src/tool/egfrCalculator/i18n/ko.ts +242 -0
  69. package/src/tool/egfrCalculator/i18n/nl.ts +242 -0
  70. package/src/tool/egfrCalculator/i18n/pl.ts +242 -0
  71. package/src/tool/egfrCalculator/i18n/pt.ts +242 -0
  72. package/src/tool/egfrCalculator/i18n/ru.ts +242 -0
  73. package/src/tool/egfrCalculator/i18n/sv.ts +242 -0
  74. package/src/tool/egfrCalculator/i18n/tr.ts +242 -0
  75. package/src/tool/egfrCalculator/i18n/zh.ts +242 -0
  76. package/src/tool/egfrCalculator/index.ts +12 -0
  77. package/src/tool/egfrCalculator/logic.ts +69 -0
  78. package/src/tool/egfrCalculator/seo.astro +15 -0
  79. package/src/tool/egfrCalculator/ui.ts +33 -0
  80. package/src/tools.ts +4 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jjlmoya/utils-health",
3
- "version": "1.35.0",
3
+ "version": "1.37.0",
4
4
  "type": "module",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -31,10 +31,13 @@ import { targetHeightEstimator } from '../tool/targetHeightEstimator/entry';
31
31
  import { correctedReticulocyteIndex } from '../tool/correctedReticulocyteIndex/entry';
32
32
  import { estimatedBloodVolumeCalculator } from '../tool/estimatedBloodVolumeCalculator/entry';
33
33
  import { anionGapCalculator } from '../tool/anionGapCalculator/entry';
34
+ import { creatinineClearanceCockcroftGault } from '../tool/creatinineClearanceCockcroftGault/entry';
35
+ import { correctedCalciumCalculator } from '../tool/correctedCalciumCalculator/entry';
36
+ import { egfrCalculator } from '../tool/egfrCalculator/entry';
34
37
 
35
38
  export const healthCategory: HealthCategoryEntry = {
36
39
  icon: 'mdi:heart-pulse',
37
- tools: [bmiCalculator, amputationAdjustedBmiCalculator, bodyAdiposityIndexCalculator, bodyFatPercentageCalculator, waistHipRatioCalculator, waistHeightRatioCalculator, leanBodyMassCalculator, metabolicAgeCalculator, caloricDeficitCalculator, dailyProteinCalculator, macronutrientCalculator, tdeeCalculator, bunCreatinineCalculator, serumOsmolalityCalculator, meanArterialPressureCalculator, correctedSodiumHyperglycemiaCalculator, fractionalExcretionSodiumCalculator, correctedReticulocyteIndex, estimatedBloodVolumeCalculator, anionGapCalculator, targetHeightEstimator, daltonismSimulator, tinnitusReliever, parametricNoiseSynthesizer, breathingVisualizer, caffeineTracker, waterPurifier, digestionStopwatch, binauralTuner, pelliRobsonTest, peripheralVisionTrainer, epworthSleepinessScale],
40
+ tools: [bmiCalculator, amputationAdjustedBmiCalculator, bodyAdiposityIndexCalculator, bodyFatPercentageCalculator, waistHipRatioCalculator, waistHeightRatioCalculator, leanBodyMassCalculator, metabolicAgeCalculator, caloricDeficitCalculator, dailyProteinCalculator, macronutrientCalculator, tdeeCalculator, bunCreatinineCalculator, serumOsmolalityCalculator, meanArterialPressureCalculator, correctedSodiumHyperglycemiaCalculator, fractionalExcretionSodiumCalculator, correctedReticulocyteIndex, estimatedBloodVolumeCalculator, anionGapCalculator, creatinineClearanceCockcroftGault, correctedCalciumCalculator, egfrCalculator, targetHeightEstimator, daltonismSimulator, tinnitusReliever, parametricNoiseSynthesizer, breathingVisualizer, caffeineTracker, waterPurifier, digestionStopwatch, binauralTuner, pelliRobsonTest, peripheralVisionTrainer, epworthSleepinessScale],
38
41
  i18n: {
39
42
  en: () => import('./i18n/en').then((m) => m.content),
40
43
  es: () => import('./i18n/es').then((m) => m.content),
package/src/entries.ts CHANGED
@@ -27,6 +27,9 @@ export { targetHeightEstimator } from './tool/targetHeightEstimator/entry';
27
27
  export { correctedReticulocyteIndex } from './tool/correctedReticulocyteIndex/entry';
28
28
  export { estimatedBloodVolumeCalculator } from './tool/estimatedBloodVolumeCalculator/entry';
29
29
  export { anionGapCalculator } from './tool/anionGapCalculator/entry';
30
+ export { creatinineClearanceCockcroftGault } from './tool/creatinineClearanceCockcroftGault/entry';
31
+ export { correctedCalciumCalculator } from './tool/correctedCalciumCalculator/entry';
32
+ export { egfrCalculator } from './tool/egfrCalculator/entry';
30
33
  export { pelliRobsonTest } from './tool/pelliRobsonTest/entry';
31
34
  export { peripheralVisionTrainer } from './tool/peripheralVisionTrainer/entry';
32
35
  export { readingDistanceCalculator } from './tool/readingDistanceCalculator/entry';
@@ -65,6 +68,9 @@ import { targetHeightEstimator } from './tool/targetHeightEstimator/entry';
65
68
  import { correctedReticulocyteIndex } from './tool/correctedReticulocyteIndex/entry';
66
69
  import { estimatedBloodVolumeCalculator } from './tool/estimatedBloodVolumeCalculator/entry';
67
70
  import { anionGapCalculator } from './tool/anionGapCalculator/entry';
71
+ import { creatinineClearanceCockcroftGault } from './tool/creatinineClearanceCockcroftGault/entry';
72
+ import { correctedCalciumCalculator } from './tool/correctedCalciumCalculator/entry';
73
+ import { egfrCalculator } from './tool/egfrCalculator/entry';
68
74
  import { pelliRobsonTest } from './tool/pelliRobsonTest/entry';
69
75
  import { peripheralVisionTrainer } from './tool/peripheralVisionTrainer/entry';
70
76
  import { readingDistanceCalculator } from './tool/readingDistanceCalculator/entry';
@@ -73,4 +79,4 @@ import { tinnitusReliever } from './tool/tinnitusReliever/entry';
73
79
  import { parametricNoiseSynthesizer } from './tool/parametricNoiseSynthesizer/entry';
74
80
  import { ubeCalculator } from './tool/ubeCalculator/entry';
75
81
  import { waterPurifier } from './tool/waterPurifier/entry';
76
- export const ALL_ENTRIES = [binauralTuner, bloodUnitConverter, bmiCalculator, amputationAdjustedBmiCalculator, bodyAdiposityIndexCalculator, bodyFatPercentageCalculator, waistHipRatioCalculator, waistHeightRatioCalculator, breathingVisualizer, caffeineTracker, caloricDeficitCalculator, dailyProteinCalculator, macronutrientCalculator, tdeeCalculator, bunCreatinineCalculator, serumOsmolalityCalculator, meanArterialPressureCalculator, correctedSodiumHyperglycemiaCalculator, fractionalExcretionSodiumCalculator, correctedReticulocyteIndex, estimatedBloodVolumeCalculator, anionGapCalculator, targetHeightEstimator, daltonismSimulator, digestionStopwatch, epworthSleepinessScale, hydrationCalculator, leanBodyMassCalculator, metabolicAgeCalculator, pelliRobsonTest, peripheralVisionTrainer, readingDistanceCalculator, screenDecompressionTime, tinnitusReliever, parametricNoiseSynthesizer, ubeCalculator, waterPurifier];
82
+ export const ALL_ENTRIES = [binauralTuner, bloodUnitConverter, bmiCalculator, amputationAdjustedBmiCalculator, bodyAdiposityIndexCalculator, bodyFatPercentageCalculator, waistHipRatioCalculator, waistHeightRatioCalculator, breathingVisualizer, caffeineTracker, caloricDeficitCalculator, dailyProteinCalculator, macronutrientCalculator, tdeeCalculator, bunCreatinineCalculator, serumOsmolalityCalculator, meanArterialPressureCalculator, correctedSodiumHyperglycemiaCalculator, fractionalExcretionSodiumCalculator, correctedReticulocyteIndex, estimatedBloodVolumeCalculator, anionGapCalculator, creatinineClearanceCockcroftGault, correctedCalciumCalculator, egfrCalculator, targetHeightEstimator, daltonismSimulator, digestionStopwatch, epworthSleepinessScale, hydrationCalculator, leanBodyMassCalculator, metabolicAgeCalculator, pelliRobsonTest, peripheralVisionTrainer, readingDistanceCalculator, screenDecompressionTime, tinnitusReliever, parametricNoiseSynthesizer, ubeCalculator, waterPurifier];
package/src/index.ts CHANGED
@@ -35,6 +35,9 @@ export * from './tool/targetHeightEstimator';
35
35
  export * from './tool/correctedReticulocyteIndex';
36
36
  export * from './tool/estimatedBloodVolumeCalculator';
37
37
  export * from './tool/anionGapCalculator';
38
+ export * from './tool/creatinineClearanceCockcroftGault';
39
+ export * from './tool/correctedCalciumCalculator';
40
+ export * from './tool/egfrCalculator';
38
41
 
39
42
  export { healthCategory } from './category';
40
43
  export const healthCategorySEO = () => import('./category/seo.astro').then((m) => m.default);
@@ -36,6 +36,6 @@ describe('Locale Completeness Validation', () => {
36
36
  });
37
37
 
38
38
  it('all tools registered', () => {
39
- expect(ALL_TOOLS.length).toBe(37);
39
+ expect(ALL_TOOLS.length).toBe(40);
40
40
  });
41
41
  });
@@ -4,8 +4,8 @@ import { healthCategory } from '../data';
4
4
 
5
5
  describe('Tool Validation Suite', () => {
6
6
  describe('Library Registration', () => {
7
- it('should have 37 tools in ALL_TOOLS', () => {
8
- expect(ALL_TOOLS.length).toBe(37);
7
+ it('should have 40 tools in ALL_TOOLS', () => {
8
+ expect(ALL_TOOLS.length).toBe(40);
9
9
  });
10
10
 
11
11
  it('healthCategory should be defined', () => {
@@ -0,0 +1,14 @@
1
+ ---
2
+ import { Bibliography } from '@jjlmoya/utils-shared';
3
+ import { correctedCalciumCalculator } from './index';
4
+ import type { KnownLocale } from '../../types';
5
+
6
+ interface Props {
7
+ locale?: KnownLocale;
8
+ }
9
+
10
+ const { locale = 'en' } = Astro.props;
11
+ const content = await correctedCalciumCalculator.i18n[locale]?.();
12
+ ---
13
+
14
+ {content && <Bibliography links={content.bibliography} />}
@@ -0,0 +1,20 @@
1
+ import type { BibliographyEntry } from '../../types';
2
+
3
+ export const bibliography: BibliographyEntry[] = [
4
+ {
5
+ name: 'Payne RB, et al. (1973). Interpretation of serum calcium in patients with abnormal serum proteins. British Medical Journal.',
6
+ url: 'https://www.bmj.com/content/4/5893/643',
7
+ },
8
+ {
9
+ name: 'StatPearls: Hypocalcemia (clinical review of calcium homeostasis).',
10
+ url: 'https://www.ncbi.nlm.nih.gov/books/NBK560604/',
11
+ },
12
+ {
13
+ name: 'StatPearls: Hypercalcemia (clinical guidance on calcium abnormalities).',
14
+ url: 'https://www.ncbi.nlm.nih.gov/books/NBK430714/',
15
+ },
16
+ {
17
+ name: 'Wikipedia: Calcium in biology (physiology and binding characteristics).',
18
+ url: 'https://en.wikipedia.org/wiki/Calcium_in_biology',
19
+ },
20
+ ];
@@ -0,0 +1,317 @@
1
+ ---
2
+ import './corrected-calcium-calculator.css';
3
+ import type { CorrectedCalciumCalculatorUI } from './ui';
4
+
5
+ interface Props {
6
+ locale?: string;
7
+ ui?: Record<string, unknown>;
8
+ }
9
+
10
+ const t = (Astro.props.ui || {}) as CorrectedCalciumCalculatorUI;
11
+ ---
12
+
13
+ <div class="ccc" data-ui={JSON.stringify(t)}>
14
+ <div class="ccc__warning" role="alert">
15
+ <div class="ccc__warning-mark">{t.safetyMark}</div>
16
+ <div class="ccc__warning-text">
17
+ <strong class="ccc__warning-title">{t.safetyTitle}</strong>
18
+ <span>{t.safetyBody}</span>
19
+ </div>
20
+ </div>
21
+
22
+ <div class="ccc__layout">
23
+ <section class="ccc__meter-card" aria-label={t.calciumAxisLabel}>
24
+ <h3 class="ccc__meter-title">{t.calciumAxisLabel}</h3>
25
+
26
+ <div class="ccc__gauge-container">
27
+ <div class="ccc__gauge-track">
28
+ <div id="ccc-segment-low" class="ccc__gauge-segment ccc__gauge-segment--low"></div>
29
+ <div id="ccc-segment-normal" class="ccc__gauge-segment ccc__gauge-segment--normal"></div>
30
+ <div id="ccc-segment-high" class="ccc__gauge-segment ccc__gauge-segment--high"></div>
31
+ </div>
32
+
33
+ <div id="ccc-displacement-bridge" class="ccc__displacement-bridge"></div>
34
+
35
+ <div id="ccc-ptr-measured" class="ccc__pointer ccc__pointer--measured">
36
+ <span class="ccc__pointer-label ccc__pointer-label--measured">{t.measuredMarker}</span>
37
+ </div>
38
+
39
+ <div id="ccc-ptr-corrected" class="ccc__pointer ccc__pointer--corrected">
40
+ <span class="ccc__pointer-label ccc__pointer-label--corrected">{t.correctedMarker}</span>
41
+ </div>
42
+ </div>
43
+
44
+ <div class="ccc__scale-labels">
45
+ <span id="ccc-tick-min" class="ccc__scale-tick" style="left: 0%;"></span>
46
+ <span id="ccc-tick-low-val" class="ccc__scale-tick"></span>
47
+ <span id="ccc-tick-high-val" class="ccc__scale-tick"></span>
48
+ <span id="ccc-tick-max" class="ccc__scale-tick" style="left: 100%;"></span>
49
+ </div>
50
+ </section>
51
+
52
+ <section class="ccc__panel">
53
+ <div class="ccc__fields">
54
+ <div class="ccc__field">
55
+ <label class="ccc__field-title" for="ccc-calcium">{t.calciumLabel}</label>
56
+ <div class="ccc__input-group">
57
+ <input id="ccc-calcium" class="ccc__input" type="number" min="0" step="0.1" placeholder={t.calciumPlaceholder} />
58
+ <div class="ccc__toggle-group" role="group">
59
+ <button type="button" class="ccc__toggle-btn ccc__toggle-btn--active" data-calcium-unit="mg/dL">{t.calciumMgdl}</button>
60
+ <button type="button" class="ccc__toggle-btn" data-calcium-unit="mmol/L">{t.calciumMmol}</button>
61
+ </div>
62
+ </div>
63
+ </div>
64
+
65
+ <div class="ccc__field">
66
+ <label class="ccc__field-title" for="ccc-albumin">{t.albuminLabel}</label>
67
+ <div class="ccc__input-group">
68
+ <input id="ccc-albumin" class="ccc__input" type="number" min="0" step="0.1" placeholder={t.albuminPlaceholder} />
69
+ <div class="ccc__toggle-group" role="group">
70
+ <button type="button" class="ccc__toggle-btn ccc__toggle-btn--active" data-albumin-unit="g/dL">{t.albuminGdl}</button>
71
+ <button type="button" class="ccc__toggle-btn" data-albumin-unit="g/L">{t.albuminGl}</button>
72
+ </div>
73
+ </div>
74
+ </div>
75
+ </div>
76
+
77
+ <div class="ccc__results">
78
+ <div class="ccc__result-box ccc__result-box--highlight">
79
+ <span class="ccc__result-label">{t.correctedLabel}</span>
80
+ <div class="ccc__result-values">
81
+ <span id="ccc-val-corrected" class="ccc__result-number">{t.emptyValue}</span>
82
+ <span id="ccc-unit-corrected" class="ccc__result-unit"></span>
83
+ </div>
84
+ </div>
85
+
86
+ <div class="ccc__result-box">
87
+ <span class="ccc__result-label">{t.measuredLabel}</span>
88
+ <div class="ccc__result-values">
89
+ <span id="ccc-val-measured" class="ccc__result-number">{t.emptyValue}</span>
90
+ <span id="ccc-unit-measured" class="ccc__result-unit"></span>
91
+ </div>
92
+ </div>
93
+ </div>
94
+
95
+ <div id="ccc-interpretation" class="ccc__interpretation" style="display: none;"></div>
96
+
97
+ <div class="ccc__formula">
98
+ <span>{t.formulaLabel}: </span>
99
+ <code id="ccc-formula-text"></code>
100
+ </div>
101
+ </section>
102
+ </div>
103
+ </div>
104
+
105
+ <script>
106
+ type CalciumUnit = 'mg/dL' | 'mmol/L';
107
+ type AlbuminUnit = 'g/dL' | 'g/L';
108
+
109
+ interface UIStrings {
110
+ emptyValue: string;
111
+ formulaConventional: string;
112
+ formulaSi: string;
113
+ interpretationLow: string;
114
+ interpretationNormal: string;
115
+ interpretationHigh: string;
116
+ interpretationBody: string;
117
+ calciumMgdl: string;
118
+ calciumMmol: string;
119
+ }
120
+
121
+ const root = document.querySelector('.ccc');
122
+ if (root) {
123
+ const t: UIStrings = JSON.parse(root.getAttribute('data-ui') || '{}');
124
+
125
+ const inputCalcium = root.querySelector('#ccc-calcium') as HTMLInputElement;
126
+ const inputAlbumin = root.querySelector('#ccc-albumin') as HTMLInputElement;
127
+
128
+ const valMeasured = root.querySelector('#ccc-val-measured') as HTMLSpanElement;
129
+ const valCorrected = root.querySelector('#ccc-val-corrected') as HTMLSpanElement;
130
+ const unitMeasured = root.querySelector('#ccc-unit-measured') as HTMLSpanElement;
131
+ const unitCorrected = root.querySelector('#ccc-unit-corrected') as HTMLSpanElement;
132
+
133
+ const ptrMeasured = root.querySelector('#ccc-ptr-measured') as HTMLDivElement;
134
+ const ptrCorrected = root.querySelector('#ccc-ptr-corrected') as HTMLDivElement;
135
+ const bridge = root.querySelector('#ccc-displacement-bridge') as HTMLDivElement;
136
+
137
+ const segmentLow = root.querySelector('#ccc-segment-low') as HTMLDivElement;
138
+ const segmentNormal = root.querySelector('#ccc-segment-normal') as HTMLDivElement;
139
+ const segmentHigh = root.querySelector('#ccc-segment-high') as HTMLDivElement;
140
+
141
+ const tickMin = root.querySelector('#ccc-tick-min') as HTMLSpanElement;
142
+ const tickLowVal = root.querySelector('#ccc-tick-low-val') as HTMLSpanElement;
143
+ const tickHighVal = root.querySelector('#ccc-tick-high-val') as HTMLSpanElement;
144
+ const tickMax = root.querySelector('#ccc-tick-max') as HTMLSpanElement;
145
+
146
+ const interpretation = root.querySelector('#ccc-interpretation') as HTMLDivElement;
147
+ const formulaText = root.querySelector('#ccc-formula-text') as HTMLElement;
148
+
149
+ let calciumUnit: CalciumUnit = 'mg/dL';
150
+ let albuminUnit: AlbuminUnit = 'g/dL';
151
+
152
+ const getLimits = (unit: CalciumUnit) => {
153
+ if (unit === 'mg/dL') {
154
+ return { min: 5.0, max: 14.0, low: 8.5, high: 10.5 };
155
+ }
156
+ return { min: 1.25, max: 3.50, low: 2.15, high: 2.55 };
157
+ };
158
+
159
+ const updateScale = () => {
160
+ const limits = getLimits(calciumUnit);
161
+ const total = limits.max - limits.min;
162
+ const lowPct = ((limits.low - limits.min) / total) * 100;
163
+ const normalPct = ((limits.high - limits.low) / total) * 100;
164
+ const highPct = ((limits.max - limits.high) / total) * 100;
165
+
166
+ segmentLow.style.width = `${lowPct}%`;
167
+ segmentNormal.style.width = `${normalPct}%`;
168
+ segmentHigh.style.width = `${highPct}%`;
169
+
170
+ tickMin.textContent = limits.min.toFixed(unitDecimals());
171
+
172
+ tickLowVal.textContent = limits.low.toFixed(unitDecimals());
173
+ tickLowVal.style.left = `${lowPct}%`;
174
+
175
+ tickHighVal.textContent = limits.high.toFixed(unitDecimals());
176
+ tickHighVal.style.left = `${lowPct + normalPct}%`;
177
+
178
+ tickMax.textContent = limits.max.toFixed(unitDecimals());
179
+
180
+ formulaText.textContent = calciumUnit === 'mg/dL' ? t.formulaConventional : t.formulaSi;
181
+ };
182
+
183
+ const unitDecimals = () => {
184
+ return calciumUnit === 'mg/dL' ? 1 : 2;
185
+ };
186
+
187
+ const getPositionPct = (val: number, min: number, max: number) => {
188
+ const pct = ((val - min) / (max - min)) * 100;
189
+ return Math.max(0, Math.min(100, pct));
190
+ };
191
+
192
+ const getCorrectedVal = (ca: number, alb: number) => {
193
+ if (calciumUnit === 'mg/dL') {
194
+ const albGdl = albuminUnit === 'g/L' ? alb / 10 : alb;
195
+ return ca + 0.8 * (4.0 - albGdl);
196
+ }
197
+ const albGl = albuminUnit === 'g/dL' ? alb * 10 : alb;
198
+ return ca + 0.02 * (40 - albGl);
199
+ };
200
+
201
+ const updatePointers = (ca: number, corrected: number) => {
202
+ const limits = getLimits(calciumUnit);
203
+ const measuredPct = getPositionPct(ca, limits.min, limits.max);
204
+ const correctedPct = getPositionPct(corrected, limits.min, limits.max);
205
+
206
+ ptrMeasured.style.left = `${measuredPct}%`;
207
+ ptrCorrected.style.left = `${correctedPct}%`;
208
+
209
+ const startPct = Math.min(measuredPct, correctedPct);
210
+ const widthPct = Math.abs(measuredPct - correctedPct);
211
+ bridge.style.left = `${startPct}%`;
212
+ bridge.style.width = `${widthPct}%`;
213
+ };
214
+
215
+ const showInterpretation = (corrected: number) => {
216
+ const limits = getLimits(calciumUnit);
217
+ interpretation.style.display = 'block';
218
+ interpretation.className = 'ccc__interpretation';
219
+
220
+ if (corrected < limits.low) {
221
+ interpretation.classList.add('ccc__interpretation--low');
222
+ interpretation.innerHTML = `<strong>${t.interpretationLow}</strong> — ${t.interpretationBody}`;
223
+ } else if (corrected > limits.high) {
224
+ interpretation.classList.add('ccc__interpretation--high');
225
+ interpretation.innerHTML = `<strong>${t.interpretationHigh}</strong> — ${t.interpretationBody}`;
226
+ } else {
227
+ interpretation.classList.add('ccc__interpretation--normal');
228
+ interpretation.innerHTML = `<strong>${t.interpretationNormal}</strong> — ${t.interpretationBody}`;
229
+ }
230
+ };
231
+
232
+ const calculate = () => {
233
+ let ca = parseFloat(inputCalcium.value);
234
+ let alb = parseFloat(inputAlbumin.value);
235
+ const isBlank = isNaN(ca) || isNaN(alb);
236
+
237
+ if (isBlank) {
238
+ ca = calciumUnit === 'mg/dL' ? 8.0 : 2.0;
239
+ alb = albuminUnit === 'g/dL' ? 3.0 : 30;
240
+ ptrMeasured.classList.add('ccc__pointer--preview');
241
+ ptrCorrected.classList.add('ccc__pointer--preview');
242
+ valMeasured.textContent = t.emptyValue;
243
+ valCorrected.textContent = t.emptyValue;
244
+ unitMeasured.textContent = '';
245
+ unitCorrected.textContent = '';
246
+ interpretation.style.display = 'none';
247
+ } else {
248
+ ptrMeasured.classList.remove('ccc__pointer--preview');
249
+ ptrCorrected.classList.remove('ccc__pointer--preview');
250
+ }
251
+
252
+ const corrected = getCorrectedVal(ca, alb);
253
+ const decimals = unitDecimals();
254
+
255
+ if (!isBlank) {
256
+ valMeasured.textContent = ca.toFixed(decimals);
257
+ valCorrected.textContent = corrected.toFixed(decimals);
258
+ unitMeasured.textContent = calciumUnit;
259
+ unitCorrected.textContent = calciumUnit;
260
+ showInterpretation(corrected);
261
+ }
262
+
263
+ updatePointers(ca, corrected);
264
+ };
265
+
266
+ root.querySelectorAll('[data-calcium-unit]').forEach((btn) => {
267
+ btn.addEventListener('click', (e) => {
268
+ const target = e.currentTarget as HTMLButtonElement;
269
+ const newUnit = target.getAttribute('data-calcium-unit') as CalciumUnit;
270
+ if (newUnit !== calciumUnit) {
271
+ root.querySelectorAll('[data-calcium-unit]').forEach((b) => b.classList.remove('ccc__toggle-btn--active'));
272
+ target.classList.add('ccc__toggle-btn--active');
273
+
274
+ const caVal = parseFloat(inputCalcium.value);
275
+ if (!isNaN(caVal)) {
276
+ if (newUnit === 'mmol/L') {
277
+ inputCalcium.value = (caVal / 4).toFixed(2);
278
+ } else {
279
+ inputCalcium.value = (caVal * 4).toFixed(1);
280
+ }
281
+ }
282
+ calciumUnit = newUnit;
283
+ updateScale();
284
+ calculate();
285
+ }
286
+ });
287
+ });
288
+
289
+ root.querySelectorAll('[data-albumin-unit]').forEach((btn) => {
290
+ btn.addEventListener('click', (e) => {
291
+ const target = e.currentTarget as HTMLButtonElement;
292
+ const newUnit = target.getAttribute('data-albumin-unit') as AlbuminUnit;
293
+ if (newUnit !== albuminUnit) {
294
+ root.querySelectorAll('[data-albumin-unit]').forEach((b) => b.classList.remove('ccc__toggle-btn--active'));
295
+ target.classList.add('ccc__toggle-btn--active');
296
+
297
+ const albVal = parseFloat(inputAlbumin.value);
298
+ if (!isNaN(albVal)) {
299
+ if (newUnit === 'g/L') {
300
+ inputAlbumin.value = (albVal * 10).toFixed(0);
301
+ } else {
302
+ inputAlbumin.value = (albVal / 10).toFixed(1);
303
+ }
304
+ }
305
+ albuminUnit = newUnit;
306
+ calculate();
307
+ }
308
+ });
309
+ });
310
+
311
+ inputCalcium.addEventListener('input', calculate);
312
+ inputAlbumin.addEventListener('input', calculate);
313
+
314
+ updateScale();
315
+ calculate();
316
+ }
317
+ </script>