@jjlmoya/utils-health 1.28.0 → 1.29.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 +1 -1
  7. package/src/tool/fractionalExcretionSodiumCalculator/bibliography.astro +14 -0
  8. package/src/tool/fractionalExcretionSodiumCalculator/bibliography.ts +28 -0
  9. package/src/tool/fractionalExcretionSodiumCalculator/component.astro +251 -0
  10. package/src/tool/fractionalExcretionSodiumCalculator/entry.ts +27 -0
  11. package/src/tool/fractionalExcretionSodiumCalculator/fractional-excretion-sodium-calculator.css +622 -0
  12. package/src/tool/fractionalExcretionSodiumCalculator/i18n/de.ts +329 -0
  13. package/src/tool/fractionalExcretionSodiumCalculator/i18n/en.ts +329 -0
  14. package/src/tool/fractionalExcretionSodiumCalculator/i18n/es.ts +329 -0
  15. package/src/tool/fractionalExcretionSodiumCalculator/i18n/fr.ts +329 -0
  16. package/src/tool/fractionalExcretionSodiumCalculator/i18n/id.ts +329 -0
  17. package/src/tool/fractionalExcretionSodiumCalculator/i18n/it.ts +329 -0
  18. package/src/tool/fractionalExcretionSodiumCalculator/i18n/ja.ts +329 -0
  19. package/src/tool/fractionalExcretionSodiumCalculator/i18n/ko.ts +329 -0
  20. package/src/tool/fractionalExcretionSodiumCalculator/i18n/nl.ts +329 -0
  21. package/src/tool/fractionalExcretionSodiumCalculator/i18n/pl.ts +329 -0
  22. package/src/tool/fractionalExcretionSodiumCalculator/i18n/pt.ts +329 -0
  23. package/src/tool/fractionalExcretionSodiumCalculator/i18n/ru.ts +329 -0
  24. package/src/tool/fractionalExcretionSodiumCalculator/i18n/sv.ts +329 -0
  25. package/src/tool/fractionalExcretionSodiumCalculator/i18n/tr.ts +329 -0
  26. package/src/tool/fractionalExcretionSodiumCalculator/i18n/zh.ts +329 -0
  27. package/src/tool/fractionalExcretionSodiumCalculator/index.ts +12 -0
  28. package/src/tool/fractionalExcretionSodiumCalculator/logic.ts +51 -0
  29. package/src/tool/fractionalExcretionSodiumCalculator/seo.astro +15 -0
  30. package/src/tool/fractionalExcretionSodiumCalculator/ui.ts +39 -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.28.0",
3
+ "version": "1.29.0",
4
4
  "type": "module",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -22,12 +22,13 @@ 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 { serumOsmolalityCalculator } from '../tool/serumOsmolalityCalculator/entry';
25
+ import { fractionalExcretionSodiumCalculator } from '../tool/fractionalExcretionSodiumCalculator/entry';
25
26
  import { targetHeightEstimator } from '../tool/targetHeightEstimator/entry';
26
27
  import { correctedReticulocyteIndex } from '../tool/correctedReticulocyteIndex/entry';
27
28
 
28
29
  export const healthCategory: HealthCategoryEntry = {
29
30
  icon: 'mdi:heart-pulse',
30
- tools: [bmiCalculator, bodyAdiposityIndexCalculator, bodyFatPercentageCalculator, waistHipRatioCalculator, waistHeightRatioCalculator, leanBodyMassCalculator, metabolicAgeCalculator, caloricDeficitCalculator, dailyProteinCalculator, macronutrientCalculator, tdeeCalculator, bunCreatinineCalculator, serumOsmolalityCalculator, correctedReticulocyteIndex, targetHeightEstimator, daltonismSimulator, tinnitusReliever, breathingVisualizer, caffeineTracker, waterPurifier, digestionStopwatch, binauralTuner, pelliRobsonTest, peripheralVisionTrainer, epworthSleepinessScale],
31
+ tools: [bmiCalculator, bodyAdiposityIndexCalculator, bodyFatPercentageCalculator, waistHipRatioCalculator, waistHeightRatioCalculator, leanBodyMassCalculator, metabolicAgeCalculator, caloricDeficitCalculator, dailyProteinCalculator, macronutrientCalculator, tdeeCalculator, bunCreatinineCalculator, serumOsmolalityCalculator, fractionalExcretionSodiumCalculator, correctedReticulocyteIndex, targetHeightEstimator, daltonismSimulator, tinnitusReliever, breathingVisualizer, caffeineTracker, waterPurifier, digestionStopwatch, binauralTuner, pelliRobsonTest, peripheralVisionTrainer, epworthSleepinessScale],
31
32
  i18n: {
32
33
  en: () => import('./i18n/en').then((m) => m.content),
33
34
  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 { serumOsmolalityCalculator } from './tool/serumOsmolalityCalculator/entry';
22
+ export { fractionalExcretionSodiumCalculator } from './tool/fractionalExcretionSodiumCalculator/entry';
22
23
  export { targetHeightEstimator } from './tool/targetHeightEstimator/entry';
23
24
  export { correctedReticulocyteIndex } from './tool/correctedReticulocyteIndex/entry';
24
25
  export { pelliRobsonTest } from './tool/pelliRobsonTest/entry';
@@ -50,6 +51,7 @@ import { macronutrientCalculator } from './tool/macronutrientCalculator/entry';
50
51
  import { tdeeCalculator } from './tool/tdeeCalculator/entry';
51
52
  import { bunCreatinineCalculator } from './tool/bunCreatinineCalculator/entry';
52
53
  import { serumOsmolalityCalculator } from './tool/serumOsmolalityCalculator/entry';
54
+ import { fractionalExcretionSodiumCalculator } from './tool/fractionalExcretionSodiumCalculator/entry';
53
55
  import { targetHeightEstimator } from './tool/targetHeightEstimator/entry';
54
56
  import { correctedReticulocyteIndex } from './tool/correctedReticulocyteIndex/entry';
55
57
  import { pelliRobsonTest } from './tool/pelliRobsonTest/entry';
@@ -59,4 +61,4 @@ import { screenDecompressionTime } from './tool/screenDecompressionTime/entry';
59
61
  import { tinnitusReliever } from './tool/tinnitusReliever/entry';
60
62
  import { ubeCalculator } from './tool/ubeCalculator/entry';
61
63
  import { waterPurifier } from './tool/waterPurifier/entry';
62
- export const ALL_ENTRIES = [binauralTuner, bloodUnitConverter, bmiCalculator, bodyAdiposityIndexCalculator, bodyFatPercentageCalculator, waistHipRatioCalculator, waistHeightRatioCalculator, breathingVisualizer, caffeineTracker, caloricDeficitCalculator, dailyProteinCalculator, macronutrientCalculator, tdeeCalculator, bunCreatinineCalculator, serumOsmolalityCalculator, correctedReticulocyteIndex, targetHeightEstimator, daltonismSimulator, digestionStopwatch, epworthSleepinessScale, hydrationCalculator, leanBodyMassCalculator, metabolicAgeCalculator, pelliRobsonTest, peripheralVisionTrainer, readingDistanceCalculator, screenDecompressionTime, tinnitusReliever, ubeCalculator, waterPurifier];
64
+ export const ALL_ENTRIES = [binauralTuner, bloodUnitConverter, bmiCalculator, bodyAdiposityIndexCalculator, bodyFatPercentageCalculator, waistHipRatioCalculator, waistHeightRatioCalculator, breathingVisualizer, caffeineTracker, caloricDeficitCalculator, dailyProteinCalculator, macronutrientCalculator, tdeeCalculator, bunCreatinineCalculator, serumOsmolalityCalculator, fractionalExcretionSodiumCalculator, 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/serumOsmolalityCalculator';
29
+ export * from './tool/fractionalExcretionSodiumCalculator';
29
30
  export * from './tool/targetHeightEstimator';
30
31
  export * from './tool/correctedReticulocyteIndex';
31
32
 
@@ -36,6 +36,6 @@ describe('Locale Completeness Validation', () => {
36
36
  });
37
37
 
38
38
  it('all tools registered', () => {
39
- expect(ALL_TOOLS.length).toBe(30);
39
+ expect(ALL_TOOLS.length).toBe(31);
40
40
  });
41
41
  });
@@ -5,7 +5,7 @@ import { healthCategory } from '../data';
5
5
  describe('Tool Validation Suite', () => {
6
6
  describe('Library Registration', () => {
7
7
  it('should have 30 tools in ALL_TOOLS', () => {
8
- expect(ALL_TOOLS.length).toBe(30);
8
+ expect(ALL_TOOLS.length).toBe(31);
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 { fractionalExcretionSodiumCalculator } 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 fractionalExcretionSodiumCalculator.i18n[locale]?.();
12
+ ---
13
+
14
+ {content && <Bibliography links={content.bibliography} />}
@@ -0,0 +1,28 @@
1
+ import type { BibliographyEntry } from '../../types';
2
+
3
+ export const bibliography: BibliographyEntry[] = [
4
+ {
5
+ name: 'Espinel CH. The FENa test. Use in the differential diagnosis of acute renal failure. JAMA. 1976.',
6
+ url: 'https://pubmed.ncbi.nlm.nih.gov/947998/',
7
+ },
8
+ {
9
+ name: 'Steiner RW. Interpreting the fractional excretion of sodium. American Journal of Medicine. 1984.',
10
+ url: 'https://pubmed.ncbi.nlm.nih.gov/6507441/',
11
+ },
12
+ {
13
+ name: 'Miller TR, Anderson RJ, Linas SL, et al. Urinary diagnostic indices in acute renal failure. Annals of Internal Medicine. 1978.',
14
+ url: 'https://pubmed.ncbi.nlm.nih.gov/677543/',
15
+ },
16
+ {
17
+ name: 'KDIGO Clinical Practice Guideline for Acute Kidney Injury. Kidney International Supplements. 2012.',
18
+ url: 'https://kdigo.org/guidelines/acute-kidney-injury/',
19
+ },
20
+ {
21
+ name: 'Merck Manual Professional Edition: Acute Kidney Injury',
22
+ url: 'https://www.merckmanuals.com/professional/genitourinary-disorders/acute-kidney-injury/acute-kidney-injury-aki',
23
+ },
24
+ {
25
+ name: 'Wikipedia: Fractional excretion of sodium',
26
+ url: 'https://en.wikipedia.org/wiki/Fractional_excretion_of_sodium',
27
+ },
28
+ ];
@@ -0,0 +1,251 @@
1
+ ---
2
+ import './fractional-excretion-sodium-calculator.css';
3
+ import type { FractionalExcretionSodiumCalculatorUI } from './ui';
4
+
5
+ interface Props {
6
+ locale?: string;
7
+ ui?: Record<string, unknown>;
8
+ }
9
+
10
+ const t = (Astro.props.ui || {}) as FractionalExcretionSodiumCalculatorUI;
11
+ ---
12
+
13
+ <div class="fena" data-ui={JSON.stringify(t)}>
14
+ <div class="fena__warning" role="alert" aria-label={t.educationWarningTitle}>
15
+ <div class="fena__warning-mark">{t.educationWarningMark}</div>
16
+ <div>
17
+ <strong>{t.educationWarningTitle}</strong>
18
+ <span>{t.educationWarningBody}</span>
19
+ </div>
20
+ </div>
21
+
22
+ <div class="fena__grid">
23
+ <section class="fena__panel fena__panel--viz">
24
+ <div class="fena__dial" aria-label={t.resultLabel}>
25
+ <div class="fena__dial-arc"></div>
26
+ <div id="fena-needle" class="fena__needle"></div>
27
+ <div class="fena__dial-center">
28
+ <span>{t.resultLabel}</span>
29
+ <strong id="fena-value">{t.emptyValue}</strong>
30
+ <small>{t.percentUnit}</small>
31
+ </div>
32
+ <span class="fena__scale fena__scale--low">{t.scaleLow}</span>
33
+ <span class="fena__scale fena__scale--mid">{t.scaleMid}</span>
34
+ <span class="fena__scale fena__scale--high">{t.scaleHigh}</span>
35
+ </div>
36
+
37
+ <div id="fena-band" class="fena__band fena__band--empty">
38
+ <strong id="fena-band-title">{t.enterValues}</strong>
39
+ <span id="fena-band-body">{t.formula}</span>
40
+ </div>
41
+
42
+ <div class="fena__kidney" aria-label={t.kidneyFilterLabel}>
43
+ <svg viewBox="0 0 320 170" role="img" aria-hidden="true">
44
+ <path class="fena__kidney-shape" d="M118 16c-41 4-73 39-73 82 0 36 21 63 51 63 22 0 32-17 32-38 0-17-9-31-9-48 0-22 15-40 37-48-11-8-23-12-38-11Z"></path>
45
+ <path class="fena__kidney-shape fena__kidney-shape--right" d="M202 16c41 4 73 39 73 82 0 36-21 63-51 63-22 0-32-17-32-38 0-17 9-31 9-48 0-22-15-40-37-48 11-8 23-12 38-11Z"></path>
46
+ <path class="fena__tube" d="M82 82c48 0 53 41 78 41s30-41 78-41"></path>
47
+ <path id="fena-reabsorb-flow" class="fena__flow fena__flow--reabsorb" d="M88 101c40 15 56 24 72 24s32-9 72-24"></path>
48
+ <path id="fena-excrete-flow" class="fena__flow fena__flow--excrete" d="M160 124v37"></path>
49
+ <circle class="fena__na fena__na--a" cx="86" cy="82" r="7"></circle>
50
+ <circle class="fena__na fena__na--b" cx="160" cy="123" r="7"></circle>
51
+ <circle class="fena__na fena__na--c" cx="235" cy="82" r="7"></circle>
52
+ </svg>
53
+ <div class="fena__flow-labels">
54
+ <span>{t.sodiumReabsorbedLabel}</span>
55
+ <span>{t.sodiumExcretedLabel}</span>
56
+ </div>
57
+ </div>
58
+ </section>
59
+
60
+ <section class="fena__panel fena__panel--inputs">
61
+ <label class="fena__field">
62
+ <span>{t.serumSodiumLabel}</span>
63
+ <div class="fena__input-shell">
64
+ <input id="fena-serum-na" type="number" min="0" step="0.1" placeholder={t.serumSodiumPlaceholder} />
65
+ <small>{t.sodiumUnit}</small>
66
+ </div>
67
+ </label>
68
+
69
+ <label class="fena__field">
70
+ <span>{t.urineSodiumLabel}</span>
71
+ <div class="fena__input-shell">
72
+ <input id="fena-urine-na" type="number" min="0" step="0.1" placeholder={t.urineSodiumPlaceholder} />
73
+ <small>{t.sodiumUnit}</small>
74
+ </div>
75
+ </label>
76
+
77
+ <label class="fena__field">
78
+ <span>{t.serumCreatinineLabel}</span>
79
+ <div class="fena__input-shell fena__input-shell--toggle">
80
+ <input id="fena-serum-cr" type="number" min="0" step="0.01" placeholder={t.serumCreatininePlaceholder} />
81
+ <div class="fena__toggle" role="group" aria-label={t.serumCreatinineLabel}>
82
+ <button type="button" class="fena__toggle-btn fena__toggle-btn--active" data-serum-cr-unit="mg/dL">{t.mgdl}</button>
83
+ <button type="button" class="fena__toggle-btn" data-serum-cr-unit="umol/L">{t.umol}</button>
84
+ </div>
85
+ </div>
86
+ <small id="fena-serum-cr-converted" class="fena__converted"></small>
87
+ </label>
88
+
89
+ <label class="fena__field">
90
+ <span>{t.urineCreatinineLabel}</span>
91
+ <div class="fena__input-shell fena__input-shell--toggle">
92
+ <input id="fena-urine-cr" type="number" min="0" step="0.1" placeholder={t.urineCreatininePlaceholder} />
93
+ <div class="fena__toggle" role="group" aria-label={t.urineCreatinineLabel}>
94
+ <button type="button" class="fena__toggle-btn fena__toggle-btn--active" data-urine-cr-unit="mg/dL">{t.mgdl}</button>
95
+ <button type="button" class="fena__toggle-btn" data-urine-cr-unit="umol/L">{t.umol}</button>
96
+ </div>
97
+ </div>
98
+ <small id="fena-urine-cr-converted" class="fena__converted"></small>
99
+ </label>
100
+
101
+ <div class="fena__formula">
102
+ <span>{t.formulaLabel}</span>
103
+ <code>{t.formula}</code>
104
+ </div>
105
+
106
+ <div class="fena__caveat">
107
+ <strong>{t.caveatTitle}</strong>
108
+ <span>{t.caveatBody}</span>
109
+ </div>
110
+ </section>
111
+ </div>
112
+ </div>
113
+
114
+ <script>
115
+ type CreatinineUnit = 'mg/dL' | 'umol/L';
116
+ type FenaBand = 'prerenal' | 'indeterminate' | 'intrinsic';
117
+
118
+ interface FenaCtx {
119
+ root: Element;
120
+ ui: Record<string, string>;
121
+ serumNa: HTMLInputElement;
122
+ urineNa: HTMLInputElement;
123
+ serumCr: HTMLInputElement;
124
+ urineCr: HTMLInputElement;
125
+ serumCrUnit: CreatinineUnit;
126
+ urineCrUnit: CreatinineUnit;
127
+ }
128
+
129
+ const CR_FACTOR = 88.4;
130
+
131
+ function fenaQs<T extends HTMLElement | SVGElement>(root: Element, selector: string): T {
132
+ const el = root.querySelector<T>(selector);
133
+ if (!el) throw new Error(`Missing ${selector}`);
134
+ return el;
135
+ }
136
+
137
+ function toMgdl(value: number, unit: CreatinineUnit): number {
138
+ return unit === 'umol/L' ? value / CR_FACTOR : value;
139
+ }
140
+
141
+ function fmt(value: number, digits = 2): string {
142
+ return String(Math.round(value * 10 ** digits) / 10 ** digits);
143
+ }
144
+
145
+ function bandFor(value: number): FenaBand {
146
+ if (value < 1) return 'prerenal';
147
+ if (value <= 2) return 'indeterminate';
148
+ return 'intrinsic';
149
+ }
150
+
151
+ function setBand(ctx: FenaCtx, band: FenaBand | 'empty') {
152
+ const title = fenaQs<HTMLElement>(ctx.root, '#fena-band-title');
153
+ const body = fenaQs<HTMLElement>(ctx.root, '#fena-band-body');
154
+ const box = fenaQs<HTMLElement>(ctx.root, '#fena-band');
155
+ box.className = `fena__band fena__band--${band}`;
156
+ if (band === 'empty') {
157
+ title.textContent = ctx.ui.enterValues;
158
+ body.textContent = ctx.ui.formula;
159
+ return;
160
+ }
161
+ title.textContent = ctx.ui[`${band}Label`];
162
+ body.textContent = ctx.ui[`${band}Body`];
163
+ }
164
+
165
+ function resetFena(ctx: FenaCtx) {
166
+ fenaQs<HTMLElement>(ctx.root, '#fena-value').textContent = ctx.ui.emptyValue;
167
+ fenaQs<HTMLElement>(ctx.root, '#fena-needle').style.transform = 'translateX(-50%) rotate(-82deg)';
168
+ fenaQs<SVGPathElement>(ctx.root, '#fena-reabsorb-flow').style.strokeWidth = '12';
169
+ fenaQs<SVGPathElement>(ctx.root, '#fena-excrete-flow').style.strokeWidth = '4';
170
+ fenaQs<HTMLElement>(ctx.root, '#fena-serum-cr-converted').textContent = '';
171
+ fenaQs<HTMLElement>(ctx.root, '#fena-urine-cr-converted').textContent = '';
172
+ setBand(ctx, 'empty');
173
+ }
174
+
175
+ function renderConversions(ctx: FenaCtx, serumCrMgdl: number, urineCrMgdl: number) {
176
+ fenaQs<HTMLElement>(ctx.root, '#fena-serum-cr-converted').textContent =
177
+ ctx.serumCrUnit === 'mg/dL'
178
+ ? `${ctx.ui.serumCreatinineConverted}: ${fmt(serumCrMgdl * CR_FACTOR, 1)} ${ctx.ui.umol}`
179
+ : `${ctx.ui.serumCreatinineConverted}: ${fmt(serumCrMgdl)} ${ctx.ui.mgdl}`;
180
+ fenaQs<HTMLElement>(ctx.root, '#fena-urine-cr-converted').textContent =
181
+ ctx.urineCrUnit === 'mg/dL'
182
+ ? `${ctx.ui.urineCreatinineConverted}: ${fmt(urineCrMgdl * CR_FACTOR, 0)} ${ctx.ui.umol}`
183
+ : `${ctx.ui.urineCreatinineConverted}: ${fmt(urineCrMgdl)} ${ctx.ui.mgdl}`;
184
+ }
185
+
186
+ function renderFena(ctx: FenaCtx) {
187
+ const serumNa = parseFloat(ctx.serumNa.value);
188
+ const urineNa = parseFloat(ctx.urineNa.value);
189
+ const serumCrRaw = parseFloat(ctx.serumCr.value);
190
+ const urineCrRaw = parseFloat(ctx.urineCr.value);
191
+ const hasValues = serumNa > 0 && urineNa >= 0 && serumCrRaw > 0 && urineCrRaw > 0;
192
+
193
+ if (!hasValues) {
194
+ resetFena(ctx);
195
+ return;
196
+ }
197
+
198
+ const serumCrMgdl = toMgdl(serumCrRaw, ctx.serumCrUnit);
199
+ const urineCrMgdl = toMgdl(urineCrRaw, ctx.urineCrUnit);
200
+ const fena = ((urineNa * serumCrMgdl) / (serumNa * urineCrMgdl)) * 100;
201
+ const rounded = Math.round(fena * 100) / 100;
202
+ const band = bandFor(rounded);
203
+ const angle = -82 + Math.min(1, rounded / 4) * 164;
204
+ const excretion = Math.max(4, Math.min(16, 4 + rounded * 3));
205
+ const reabsorb = Math.max(4, 14 - rounded * 2.2);
206
+
207
+ fenaQs<HTMLElement>(ctx.root, '#fena-value').textContent = fmt(rounded);
208
+ fenaQs<HTMLElement>(ctx.root, '#fena-needle').style.transform = `translateX(-50%) rotate(${angle}deg)`;
209
+ fenaQs<SVGPathElement>(ctx.root, '#fena-reabsorb-flow').style.strokeWidth = String(reabsorb);
210
+ fenaQs<SVGPathElement>(ctx.root, '#fena-excrete-flow').style.strokeWidth = String(excretion);
211
+ setBand(ctx, band);
212
+ renderConversions(ctx, serumCrMgdl, urineCrMgdl);
213
+ }
214
+
215
+ function bindUnit(root: Element, selector: string, activeClass: string, onChange: (unit: CreatinineUnit) => void) {
216
+ root.querySelectorAll<HTMLButtonElement>(selector).forEach((btn) => {
217
+ btn.addEventListener('click', () => {
218
+ const unit = (btn.dataset.serumCrUnit || btn.dataset.urineCrUnit) as CreatinineUnit;
219
+ onChange(unit);
220
+ root.querySelectorAll<HTMLButtonElement>(selector).forEach((item) => item.classList.toggle(activeClass, item === btn));
221
+ });
222
+ });
223
+ }
224
+
225
+ function initFena(root: Element) {
226
+ const ctx: FenaCtx = {
227
+ root,
228
+ ui: JSON.parse((root as HTMLElement).dataset.ui || '{}') as Record<string, string>,
229
+ serumNa: fenaQs<HTMLInputElement>(root, '#fena-serum-na'),
230
+ urineNa: fenaQs<HTMLInputElement>(root, '#fena-urine-na'),
231
+ serumCr: fenaQs<HTMLInputElement>(root, '#fena-serum-cr'),
232
+ urineCr: fenaQs<HTMLInputElement>(root, '#fena-urine-cr'),
233
+ serumCrUnit: 'mg/dL',
234
+ urineCrUnit: 'mg/dL',
235
+ };
236
+
237
+ bindUnit(root, '[data-serum-cr-unit]', 'fena__toggle-btn--active', (unit) => {
238
+ ctx.serumCrUnit = unit;
239
+ renderFena(ctx);
240
+ });
241
+ bindUnit(root, '[data-urine-cr-unit]', 'fena__toggle-btn--active', (unit) => {
242
+ ctx.urineCrUnit = unit;
243
+ renderFena(ctx);
244
+ });
245
+
246
+ [ctx.serumNa, ctx.urineNa, ctx.serumCr, ctx.urineCr].forEach((input) => input.addEventListener('input', () => renderFena(ctx)));
247
+ renderFena(ctx);
248
+ }
249
+
250
+ document.querySelectorAll('.fena').forEach(initFena);
251
+ </script>
@@ -0,0 +1,27 @@
1
+ import type { HealthToolEntry } from '../../types';
2
+ import type { FractionalExcretionSodiumCalculatorUI } from './ui';
3
+
4
+ export const fractionalExcretionSodiumCalculator: HealthToolEntry<FractionalExcretionSodiumCalculatorUI> = {
5
+ id: 'fractionalExcretionSodiumCalculator',
6
+ icons: {
7
+ bg: 'mdi:flask-outline',
8
+ fg: 'mdi:percent-outline',
9
+ },
10
+ i18n: {
11
+ de: () => import('./i18n/de').then((m) => m.content),
12
+ en: () => import('./i18n/en').then((m) => m.content),
13
+ es: () => import('./i18n/es').then((m) => m.content),
14
+ fr: () => import('./i18n/fr').then((m) => m.content),
15
+ id: () => import('./i18n/id').then((m) => m.content),
16
+ it: () => import('./i18n/it').then((m) => m.content),
17
+ ja: () => import('./i18n/ja').then((m) => m.content),
18
+ ko: () => import('./i18n/ko').then((m) => m.content),
19
+ nl: () => import('./i18n/nl').then((m) => m.content),
20
+ pl: () => import('./i18n/pl').then((m) => m.content),
21
+ pt: () => import('./i18n/pt').then((m) => m.content),
22
+ ru: () => import('./i18n/ru').then((m) => m.content),
23
+ sv: () => import('./i18n/sv').then((m) => m.content),
24
+ tr: () => import('./i18n/tr').then((m) => m.content),
25
+ zh: () => import('./i18n/zh').then((m) => m.content),
26
+ },
27
+ };