@jjlmoya/utils-health 1.26.0 → 1.27.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 (31) hide show
  1. package/package.json +1 -1
  2. package/src/category/index.ts +2 -1
  3. package/src/entries.ts +3 -1
  4. package/src/index.ts +1 -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/correctedReticulocyteIndex/bibliography.astro +14 -0
  8. package/src/tool/correctedReticulocyteIndex/bibliography.ts +20 -0
  9. package/src/tool/correctedReticulocyteIndex/component.astro +262 -0
  10. package/src/tool/correctedReticulocyteIndex/corrected-reticulocyte-index-calculator.css +419 -0
  11. package/src/tool/correctedReticulocyteIndex/entry.ts +27 -0
  12. package/src/tool/correctedReticulocyteIndex/i18n/de.ts +349 -0
  13. package/src/tool/correctedReticulocyteIndex/i18n/en.ts +349 -0
  14. package/src/tool/correctedReticulocyteIndex/i18n/es.ts +349 -0
  15. package/src/tool/correctedReticulocyteIndex/i18n/fr.ts +349 -0
  16. package/src/tool/correctedReticulocyteIndex/i18n/id.ts +349 -0
  17. package/src/tool/correctedReticulocyteIndex/i18n/it.ts +349 -0
  18. package/src/tool/correctedReticulocyteIndex/i18n/ja.ts +349 -0
  19. package/src/tool/correctedReticulocyteIndex/i18n/ko.ts +349 -0
  20. package/src/tool/correctedReticulocyteIndex/i18n/nl.ts +349 -0
  21. package/src/tool/correctedReticulocyteIndex/i18n/pl.ts +349 -0
  22. package/src/tool/correctedReticulocyteIndex/i18n/pt.ts +349 -0
  23. package/src/tool/correctedReticulocyteIndex/i18n/ru.ts +349 -0
  24. package/src/tool/correctedReticulocyteIndex/i18n/sv.ts +349 -0
  25. package/src/tool/correctedReticulocyteIndex/i18n/tr.ts +349 -0
  26. package/src/tool/correctedReticulocyteIndex/i18n/zh.ts +349 -0
  27. package/src/tool/correctedReticulocyteIndex/index.ts +12 -0
  28. package/src/tool/correctedReticulocyteIndex/logic.ts +45 -0
  29. package/src/tool/correctedReticulocyteIndex/seo.astro +15 -0
  30. package/src/tool/correctedReticulocyteIndex/ui.ts +38 -0
  31. package/src/tools.ts +2 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jjlmoya/utils-health",
3
- "version": "1.26.0",
3
+ "version": "1.27.0",
4
4
  "type": "module",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -22,10 +22,11 @@ import { leanBodyMassCalculator } from '../tool/leanBodyMassCalculator/entry';
22
22
  import { metabolicAgeCalculator } from '../tool/metabolicAgeCalculator/entry';
23
23
  import { bunCreatinineCalculator } from '../tool/bunCreatinineCalculator/entry';
24
24
  import { targetHeightEstimator } from '../tool/targetHeightEstimator/entry';
25
+ import { correctedReticulocyteIndex } from '../tool/correctedReticulocyteIndex/entry';
25
26
 
26
27
  export const healthCategory: HealthCategoryEntry = {
27
28
  icon: 'mdi:heart-pulse',
28
- tools: [bmiCalculator, bodyAdiposityIndexCalculator, bodyFatPercentageCalculator, waistHipRatioCalculator, waistHeightRatioCalculator, leanBodyMassCalculator, metabolicAgeCalculator, caloricDeficitCalculator, dailyProteinCalculator, macronutrientCalculator, tdeeCalculator, bunCreatinineCalculator, targetHeightEstimator, daltonismSimulator, tinnitusReliever, breathingVisualizer, caffeineTracker, waterPurifier, digestionStopwatch, binauralTuner, pelliRobsonTest, peripheralVisionTrainer, epworthSleepinessScale],
29
+ tools: [bmiCalculator, bodyAdiposityIndexCalculator, bodyFatPercentageCalculator, waistHipRatioCalculator, waistHeightRatioCalculator, leanBodyMassCalculator, metabolicAgeCalculator, caloricDeficitCalculator, dailyProteinCalculator, macronutrientCalculator, tdeeCalculator, bunCreatinineCalculator, correctedReticulocyteIndex, targetHeightEstimator, daltonismSimulator, tinnitusReliever, breathingVisualizer, caffeineTracker, waterPurifier, digestionStopwatch, binauralTuner, pelliRobsonTest, peripheralVisionTrainer, epworthSleepinessScale],
29
30
  i18n: {
30
31
  en: () => import('./i18n/en').then((m) => m.content),
31
32
  es: () => import('./i18n/es').then((m) => m.content),
package/src/entries.ts CHANGED
@@ -19,6 +19,7 @@ export { macronutrientCalculator } from './tool/macronutrientCalculator/entry';
19
19
  export { tdeeCalculator } from './tool/tdeeCalculator/entry';
20
20
  export { bunCreatinineCalculator } from './tool/bunCreatinineCalculator/entry';
21
21
  export { targetHeightEstimator } from './tool/targetHeightEstimator/entry';
22
+ export { correctedReticulocyteIndex } from './tool/correctedReticulocyteIndex/entry';
22
23
  export { pelliRobsonTest } from './tool/pelliRobsonTest/entry';
23
24
  export { peripheralVisionTrainer } from './tool/peripheralVisionTrainer/entry';
24
25
  export { readingDistanceCalculator } from './tool/readingDistanceCalculator/entry';
@@ -48,6 +49,7 @@ import { macronutrientCalculator } from './tool/macronutrientCalculator/entry';
48
49
  import { tdeeCalculator } from './tool/tdeeCalculator/entry';
49
50
  import { bunCreatinineCalculator } from './tool/bunCreatinineCalculator/entry';
50
51
  import { targetHeightEstimator } from './tool/targetHeightEstimator/entry';
52
+ import { correctedReticulocyteIndex } from './tool/correctedReticulocyteIndex/entry';
51
53
  import { pelliRobsonTest } from './tool/pelliRobsonTest/entry';
52
54
  import { peripheralVisionTrainer } from './tool/peripheralVisionTrainer/entry';
53
55
  import { readingDistanceCalculator } from './tool/readingDistanceCalculator/entry';
@@ -55,4 +57,4 @@ import { screenDecompressionTime } from './tool/screenDecompressionTime/entry';
55
57
  import { tinnitusReliever } from './tool/tinnitusReliever/entry';
56
58
  import { ubeCalculator } from './tool/ubeCalculator/entry';
57
59
  import { waterPurifier } from './tool/waterPurifier/entry';
58
- export const ALL_ENTRIES = [binauralTuner, bloodUnitConverter, bmiCalculator, bodyAdiposityIndexCalculator, bodyFatPercentageCalculator, waistHipRatioCalculator, waistHeightRatioCalculator, breathingVisualizer, caffeineTracker, caloricDeficitCalculator, dailyProteinCalculator, macronutrientCalculator, tdeeCalculator, bunCreatinineCalculator, targetHeightEstimator, daltonismSimulator, digestionStopwatch, epworthSleepinessScale, hydrationCalculator, leanBodyMassCalculator, metabolicAgeCalculator, pelliRobsonTest, peripheralVisionTrainer, readingDistanceCalculator, screenDecompressionTime, tinnitusReliever, ubeCalculator, waterPurifier];
60
+ export const ALL_ENTRIES = [binauralTuner, bloodUnitConverter, bmiCalculator, bodyAdiposityIndexCalculator, bodyFatPercentageCalculator, waistHipRatioCalculator, waistHeightRatioCalculator, breathingVisualizer, caffeineTracker, caloricDeficitCalculator, dailyProteinCalculator, macronutrientCalculator, tdeeCalculator, bunCreatinineCalculator, correctedReticulocyteIndex, targetHeightEstimator, daltonismSimulator, digestionStopwatch, epworthSleepinessScale, hydrationCalculator, leanBodyMassCalculator, metabolicAgeCalculator, pelliRobsonTest, peripheralVisionTrainer, readingDistanceCalculator, screenDecompressionTime, tinnitusReliever, ubeCalculator, waterPurifier];
package/src/index.ts CHANGED
@@ -26,6 +26,7 @@ export * from './tool/macronutrientCalculator';
26
26
  export * from './tool/tdeeCalculator';
27
27
  export * from './tool/bunCreatinineCalculator';
28
28
  export * from './tool/targetHeightEstimator';
29
+ export * from './tool/correctedReticulocyteIndex';
29
30
 
30
31
  export { healthCategory } from './category';
31
32
  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(28);
39
+ expect(ALL_TOOLS.length).toBe(29);
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 28 tools in ALL_TOOLS', () => {
8
- expect(ALL_TOOLS.length).toBe(28);
7
+ it('should have 29 tools in ALL_TOOLS', () => {
8
+ expect(ALL_TOOLS.length).toBe(29);
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 { correctedReticulocyteIndex } 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 correctedReticulocyteIndex.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: 'Hillman RS, Finch CA: Red Cell Manual',
6
+ url: 'https://openlibrary.org/books/OL5686128M/Red_cell_manual',
7
+ },
8
+ {
9
+ name: 'Panzassy AJ: Reticulocyte production index',
10
+ url: 'https://pubmed.ncbi.nlm.nih.gov/4061477/',
11
+ },
12
+ {
13
+ name: 'MedlinePlus / NIH: Reticulocyte count test',
14
+ url: 'https://medlineplus.gov/lab-tests/reticulocyte-count/',
15
+ },
16
+ {
17
+ name: 'Wikipedia: Reticulocyte production index',
18
+ url: 'https://en.wikipedia.org/wiki/Reticulocyte_production_index',
19
+ },
20
+ ];
@@ -0,0 +1,262 @@
1
+ ---
2
+ import './corrected-reticulocyte-index-calculator.css';
3
+ import type { CorrectedReticulocyteIndexUI } from './ui';
4
+
5
+ interface Props {
6
+ locale?: string;
7
+ ui?: Record<string, unknown>;
8
+ }
9
+
10
+ const t = (Astro.props.ui || {}) as CorrectedReticulocyteIndexUI;
11
+ ---
12
+
13
+ <div class="rpi" data-ui={JSON.stringify(t)}>
14
+ <div class="rpi__warning" role="note" aria-label={t.disclaimerTitle}>
15
+ <svg class="rpi__warning-icon" viewBox="0 0 24 24" aria-hidden="true">
16
+ <path d="M12 2L2.6 20.5h18.8L12 2z" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round" />
17
+ <path d="M12 8v6" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
18
+ <circle cx="12" cy="17.2" r="1.2" fill="currentColor" />
19
+ </svg>
20
+ <div>
21
+ <strong>{t.disclaimerTitle}</strong>
22
+ <span>{t.disclaimerBody}</span>
23
+ </div>
24
+ </div>
25
+
26
+ <div class="rpi__report">
27
+ <div class="rpi__inputs">
28
+ <label class="rpi__field">
29
+ <span>{t.observedLabel}</span>
30
+ <input id="rpi-observed" class="rpi__input" type="number" min="0" max="30" step="0.1" value="7" placeholder={t.observedPlaceholder} aria-label={t.observedLabel} />
31
+ </label>
32
+
33
+ <label class="rpi__field">
34
+ <span>{t.patientHctLabel}</span>
35
+ <input id="rpi-patient-hct" class="rpi__input" type="number" min="5" max="65" step="0.1" value="30" placeholder={t.patientHctPlaceholder} aria-label={t.patientHctLabel} />
36
+ </label>
37
+
38
+ <label class="rpi__field rpi__field--reference">
39
+ <span>{t.referenceHctLabel}</span>
40
+ <input id="rpi-reference-hct" class="rpi__input" type="number" min="20" max="60" step="0.1" value="45" placeholder={t.referenceHctPlaceholder} aria-label={t.referenceHctLabel} />
41
+ <div class="rpi__presets" role="group" aria-label={t.referenceHctLabel}>
42
+ <button type="button" class="rpi__preset rpi__preset--active" data-reference="45">{t.referenceMale}</button>
43
+ <button type="button" class="rpi__preset" data-reference="40">{t.referenceFemale}</button>
44
+ <button type="button" class="rpi__preset" data-reference="custom">{t.referenceCustom}</button>
45
+ </div>
46
+ </label>
47
+ </div>
48
+
49
+ <div class="rpi__result" data-band="adequate">
50
+ <div class="rpi__sphere" aria-label={t.rpiSphereLabel} role="img">
51
+ <svg viewBox="0 0 220 220" aria-hidden="true">
52
+ <defs>
53
+ <radialGradient id="rpiSphereGlow" cx="42%" cy="32%" r="70%">
54
+ <stop offset="0" stop-color="#fff" stop-opacity="0.95" />
55
+ <stop offset="0.26" stop-color="#fca5a5" stop-opacity="0.72" />
56
+ <stop offset="1" stop-color="#7f1d1d" stop-opacity="0.94" />
57
+ </radialGradient>
58
+ </defs>
59
+ <circle class="rpi__sphere-orbit" cx="110" cy="110" r="92" />
60
+ <circle class="rpi__sphere-core" cx="110" cy="110" r="76" />
61
+ <path id="rpi-sphere-fill" class="rpi__sphere-fill" d="M42 141 C75 118 139 118 178 141 L178 186 L42 186 Z" />
62
+ <circle class="rpi__sphere-shine" cx="82" cy="72" r="19" />
63
+ </svg>
64
+ <div class="rpi__sphere-value">
65
+ <span>{t.rpiLabel}</span>
66
+ <strong id="rpi-value">3.11</strong>
67
+ </div>
68
+ </div>
69
+
70
+ <div class="rpi__readout">
71
+ <span class="rpi__badge" id="rpi-badge">{t.adequateBadge}</span>
72
+ <strong id="rpi-band">{t.adequateLabel}</strong>
73
+ <p id="rpi-description">{t.adequateDescription}</p>
74
+ </div>
75
+ </div>
76
+ </div>
77
+
78
+ <div class="rpi__metrics">
79
+ <div class="rpi__metric">
80
+ <span>{t.correctedLabel}</span>
81
+ <strong id="rpi-corrected">4.67%</strong>
82
+ </div>
83
+ <div class="rpi__metric">
84
+ <span>{t.maturationFactorLabel}</span>
85
+ <strong id="rpi-factor">1.5</strong>
86
+ </div>
87
+ <div class="rpi__metric">
88
+ <span>{t.rpiLabel}</span>
89
+ <strong id="rpi-small">3.11</strong>
90
+ </div>
91
+ </div>
92
+
93
+ <div class="rpi__formula">
94
+ <div class="rpi__formula-card">
95
+ <span class="rpi__formula-mark" aria-hidden="true"></span>
96
+ <div class="rpi__formula-copy">
97
+ <strong>{t.formulaCorrectedTitle}</strong>
98
+ <span>{t.formulaCorrectedText}</span>
99
+ </div>
100
+ </div>
101
+ <div class="rpi__formula-card">
102
+ <span class="rpi__formula-mark" aria-hidden="true"></span>
103
+ <div class="rpi__formula-copy">
104
+ <strong>{t.formulaRpiTitle}</strong>
105
+ <span>{t.formulaRpiText}</span>
106
+ </div>
107
+ </div>
108
+ <div class="rpi__factor-card">
109
+ <div class="rpi__formula-copy">
110
+ <strong>{t.factorTitle}</strong>
111
+ </div>
112
+ <div class="rpi__factor-grid">
113
+ <span>{t.factorRule1}</span>
114
+ <span>{t.factorRule15}</span>
115
+ <span>{t.factorRule2}</span>
116
+ <span>{t.factorRule25}</span>
117
+ </div>
118
+ </div>
119
+ </div>
120
+ </div>
121
+
122
+ <script>
123
+ type Band = 'insufficient' | 'borderline' | 'adequate';
124
+
125
+ interface Elements {
126
+ root: HTMLElement;
127
+ observed: HTMLInputElement;
128
+ patientHct: HTMLInputElement;
129
+ referenceHct: HTMLInputElement;
130
+ presets: NodeListOf<HTMLButtonElement>;
131
+ result: HTMLElement;
132
+ rpiValue: HTMLElement;
133
+ rpiSmall: HTMLElement;
134
+ corrected: HTMLElement;
135
+ factor: HTMLElement;
136
+ badge: HTMLElement;
137
+ band: HTMLElement;
138
+ description: HTMLElement;
139
+ sphereFill: SVGPathElement;
140
+ }
141
+
142
+ function required<T extends Element>(root: Element, selector: string): T {
143
+ const element = root.querySelector<T>(selector);
144
+ if (!element) throw new Error(selector);
145
+ return element;
146
+ }
147
+
148
+ function getElements(root: Element): Elements | null {
149
+ try {
150
+ return {
151
+ root: root as HTMLElement,
152
+ observed: required<HTMLInputElement>(root, '#rpi-observed'),
153
+ patientHct: required<HTMLInputElement>(root, '#rpi-patient-hct'),
154
+ referenceHct: required<HTMLInputElement>(root, '#rpi-reference-hct'),
155
+ presets: root.querySelectorAll<HTMLButtonElement>('.rpi__preset'),
156
+ result: required<HTMLElement>(root, '.rpi__result'),
157
+ rpiValue: required<HTMLElement>(root, '#rpi-value'),
158
+ rpiSmall: required<HTMLElement>(root, '#rpi-small'),
159
+ corrected: required<HTMLElement>(root, '#rpi-corrected'),
160
+ factor: required<HTMLElement>(root, '#rpi-factor'),
161
+ badge: required<HTMLElement>(root, '#rpi-badge'),
162
+ band: required<HTMLElement>(root, '#rpi-band'),
163
+ description: required<HTMLElement>(root, '#rpi-description'),
164
+ sphereFill: required<SVGPathElement>(root, '#rpi-sphere-fill'),
165
+ };
166
+ } catch {
167
+ return null;
168
+ }
169
+ }
170
+
171
+ function factorForHct(hct: number): number {
172
+ if (hct >= 35) return 1;
173
+ if (hct >= 25) return 1.5;
174
+ if (hct >= 15) return 2;
175
+ return 2.5;
176
+ }
177
+
178
+ function classifyRpi(rpi: number): Band {
179
+ if (rpi < 2) return 'insufficient';
180
+ if (rpi >= 3) return 'adequate';
181
+ return 'borderline';
182
+ }
183
+
184
+ function round2(value: number): number {
185
+ return Math.round(value * 100) / 100;
186
+ }
187
+
188
+ function formatPercent(value: number, ui: Record<string, string>): string {
189
+ return `${round2(value).toFixed(2)}${ui.percentUnit}`;
190
+ }
191
+
192
+ function renderEmpty(elements: Elements, ui: Record<string, string>) {
193
+ elements.corrected.textContent = ui.emptyValue;
194
+ elements.factor.textContent = ui.emptyValue;
195
+ elements.rpiValue.textContent = ui.emptyValue;
196
+ elements.rpiSmall.textContent = ui.emptyValue;
197
+ }
198
+
199
+ function render(elements: Elements, ui: Record<string, string>) {
200
+ const observed = Number(elements.observed.value);
201
+ const patientHct = Number(elements.patientHct.value);
202
+ const referenceHct = Number(elements.referenceHct.value);
203
+ const hasValues = observed > 0 && patientHct > 0 && referenceHct > 0;
204
+
205
+ if (!hasValues) {
206
+ renderEmpty(elements, ui);
207
+ return;
208
+ }
209
+
210
+ const corrected = observed * (patientHct / referenceHct);
211
+ const factor = factorForHct(patientHct);
212
+ const rpi = corrected / factor;
213
+ const band = classifyRpi(rpi);
214
+ const bandCopy: Record<Band, { label: string; badge: string; description: string }> = {
215
+ insufficient: { label: ui.insufficientLabel, badge: ui.insufficientBadge, description: ui.insufficientDescription },
216
+ borderline: { label: ui.borderlineLabel, badge: ui.borderlineBadge, description: ui.borderlineDescription },
217
+ adequate: { label: ui.adequateLabel, badge: ui.adequateBadge, description: ui.adequateDescription },
218
+ };
219
+
220
+ elements.result.dataset.band = band;
221
+ elements.corrected.textContent = formatPercent(corrected, ui);
222
+ elements.factor.textContent = factor.toFixed(1);
223
+ elements.rpiValue.textContent = round2(rpi).toFixed(2);
224
+ elements.rpiSmall.textContent = round2(rpi).toFixed(2);
225
+ elements.badge.textContent = bandCopy[band].badge;
226
+ elements.band.textContent = bandCopy[band].label;
227
+ elements.description.textContent = bandCopy[band].description;
228
+
229
+ const fillLevel = Math.max(0.14, Math.min(0.86, rpi / 5));
230
+ const top = 186 - fillLevel * 124;
231
+ elements.sphereFill.setAttribute('d', `M42 ${top} C76 ${top - 14} 142 ${top - 14} 178 ${top} L178 186 L42 186 Z`);
232
+ }
233
+
234
+ function initRpi(root: Element) {
235
+ const ui = JSON.parse((root as HTMLElement).dataset.ui || '{}') as Record<string, string>;
236
+ const elements = getElements(root);
237
+ if (!elements) return;
238
+ const update = () => render(elements, ui);
239
+
240
+ elements.presets.forEach((button) => {
241
+ button.addEventListener('click', () => {
242
+ const reference = button.dataset.reference;
243
+ if (reference !== 'custom' && reference) elements.referenceHct.value = reference;
244
+ elements.presets.forEach((item) => item.classList.toggle('rpi__preset--active', item === button));
245
+ update();
246
+ });
247
+ });
248
+
249
+ [elements.observed, elements.patientHct, elements.referenceHct].forEach((input) => {
250
+ input.addEventListener('input', () => {
251
+ if (input === elements.referenceHct) {
252
+ elements.presets.forEach((item) => item.classList.toggle('rpi__preset--active', item.dataset.reference === 'custom'));
253
+ }
254
+ update();
255
+ });
256
+ });
257
+
258
+ update();
259
+ }
260
+
261
+ document.querySelectorAll('.rpi').forEach(initRpi);
262
+ </script>