@jjlmoya/utils-health 1.27.0 → 1.28.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.
- package/package.json +1 -1
- package/src/category/index.ts +2 -1
- package/src/entries.ts +3 -1
- package/src/index.ts +1 -0
- package/src/tests/locale_completeness.test.ts +1 -1
- package/src/tests/tool_validation.test.ts +2 -2
- package/src/tool/serumOsmolalityCalculator/bibliography.astro +14 -0
- package/src/tool/serumOsmolalityCalculator/bibliography.ts +24 -0
- package/src/tool/serumOsmolalityCalculator/component.astro +269 -0
- package/src/tool/serumOsmolalityCalculator/entry.ts +27 -0
- package/src/tool/serumOsmolalityCalculator/i18n/de.ts +292 -0
- package/src/tool/serumOsmolalityCalculator/i18n/en.ts +292 -0
- package/src/tool/serumOsmolalityCalculator/i18n/es.ts +292 -0
- package/src/tool/serumOsmolalityCalculator/i18n/fr.ts +292 -0
- package/src/tool/serumOsmolalityCalculator/i18n/id.ts +292 -0
- package/src/tool/serumOsmolalityCalculator/i18n/it.ts +292 -0
- package/src/tool/serumOsmolalityCalculator/i18n/ja.ts +292 -0
- package/src/tool/serumOsmolalityCalculator/i18n/ko.ts +292 -0
- package/src/tool/serumOsmolalityCalculator/i18n/nl.ts +292 -0
- package/src/tool/serumOsmolalityCalculator/i18n/pl.ts +292 -0
- package/src/tool/serumOsmolalityCalculator/i18n/pt.ts +292 -0
- package/src/tool/serumOsmolalityCalculator/i18n/ru.ts +292 -0
- package/src/tool/serumOsmolalityCalculator/i18n/sv.ts +292 -0
- package/src/tool/serumOsmolalityCalculator/i18n/tr.ts +292 -0
- package/src/tool/serumOsmolalityCalculator/i18n/zh.ts +292 -0
- package/src/tool/serumOsmolalityCalculator/index.ts +12 -0
- package/src/tool/serumOsmolalityCalculator/logic.ts +57 -0
- package/src/tool/serumOsmolalityCalculator/seo.astro +15 -0
- package/src/tool/serumOsmolalityCalculator/serum-osmolality-calculator.css +476 -0
- package/src/tool/serumOsmolalityCalculator/ui.ts +37 -0
- package/src/tools.ts +2 -1
package/package.json
CHANGED
package/src/category/index.ts
CHANGED
|
@@ -21,12 +21,13 @@ import { epworthSleepinessScale } from '../tool/epworthSleepinessScale/entry';
|
|
|
21
21
|
import { leanBodyMassCalculator } from '../tool/leanBodyMassCalculator/entry';
|
|
22
22
|
import { metabolicAgeCalculator } from '../tool/metabolicAgeCalculator/entry';
|
|
23
23
|
import { bunCreatinineCalculator } from '../tool/bunCreatinineCalculator/entry';
|
|
24
|
+
import { serumOsmolalityCalculator } from '../tool/serumOsmolalityCalculator/entry';
|
|
24
25
|
import { targetHeightEstimator } from '../tool/targetHeightEstimator/entry';
|
|
25
26
|
import { correctedReticulocyteIndex } from '../tool/correctedReticulocyteIndex/entry';
|
|
26
27
|
|
|
27
28
|
export const healthCategory: HealthCategoryEntry = {
|
|
28
29
|
icon: 'mdi:heart-pulse',
|
|
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],
|
|
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],
|
|
30
31
|
i18n: {
|
|
31
32
|
en: () => import('./i18n/en').then((m) => m.content),
|
|
32
33
|
es: () => import('./i18n/es').then((m) => m.content),
|
package/src/entries.ts
CHANGED
|
@@ -18,6 +18,7 @@ export { metabolicAgeCalculator } from './tool/metabolicAgeCalculator/entry';
|
|
|
18
18
|
export { macronutrientCalculator } from './tool/macronutrientCalculator/entry';
|
|
19
19
|
export { tdeeCalculator } from './tool/tdeeCalculator/entry';
|
|
20
20
|
export { bunCreatinineCalculator } from './tool/bunCreatinineCalculator/entry';
|
|
21
|
+
export { serumOsmolalityCalculator } from './tool/serumOsmolalityCalculator/entry';
|
|
21
22
|
export { targetHeightEstimator } from './tool/targetHeightEstimator/entry';
|
|
22
23
|
export { correctedReticulocyteIndex } from './tool/correctedReticulocyteIndex/entry';
|
|
23
24
|
export { pelliRobsonTest } from './tool/pelliRobsonTest/entry';
|
|
@@ -48,6 +49,7 @@ import { metabolicAgeCalculator } from './tool/metabolicAgeCalculator/entry';
|
|
|
48
49
|
import { macronutrientCalculator } from './tool/macronutrientCalculator/entry';
|
|
49
50
|
import { tdeeCalculator } from './tool/tdeeCalculator/entry';
|
|
50
51
|
import { bunCreatinineCalculator } from './tool/bunCreatinineCalculator/entry';
|
|
52
|
+
import { serumOsmolalityCalculator } from './tool/serumOsmolalityCalculator/entry';
|
|
51
53
|
import { targetHeightEstimator } from './tool/targetHeightEstimator/entry';
|
|
52
54
|
import { correctedReticulocyteIndex } from './tool/correctedReticulocyteIndex/entry';
|
|
53
55
|
import { pelliRobsonTest } from './tool/pelliRobsonTest/entry';
|
|
@@ -57,4 +59,4 @@ import { screenDecompressionTime } from './tool/screenDecompressionTime/entry';
|
|
|
57
59
|
import { tinnitusReliever } from './tool/tinnitusReliever/entry';
|
|
58
60
|
import { ubeCalculator } from './tool/ubeCalculator/entry';
|
|
59
61
|
import { waterPurifier } from './tool/waterPurifier/entry';
|
|
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];
|
|
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];
|
package/src/index.ts
CHANGED
|
@@ -25,6 +25,7 @@ export * from './tool/bodyFatPercentageCalculator';
|
|
|
25
25
|
export * from './tool/macronutrientCalculator';
|
|
26
26
|
export * from './tool/tdeeCalculator';
|
|
27
27
|
export * from './tool/bunCreatinineCalculator';
|
|
28
|
+
export * from './tool/serumOsmolalityCalculator';
|
|
28
29
|
export * from './tool/targetHeightEstimator';
|
|
29
30
|
export * from './tool/correctedReticulocyteIndex';
|
|
30
31
|
|
|
@@ -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
|
|
8
|
-
expect(ALL_TOOLS.length).toBe(
|
|
7
|
+
it('should have 30 tools in ALL_TOOLS', () => {
|
|
8
|
+
expect(ALL_TOOLS.length).toBe(30);
|
|
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 { serumOsmolalityCalculator } 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 serumOsmolalityCalculator.i18n[locale]?.();
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
{content && <Bibliography links={content.bibliography} />}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { BibliographyEntry } from '../../types';
|
|
2
|
+
|
|
3
|
+
export const bibliography: BibliographyEntry[] = [
|
|
4
|
+
{
|
|
5
|
+
name: 'Gennari FJ. Serum osmolality: uses and limitations. New England Journal of Medicine. 1984.',
|
|
6
|
+
url: 'https://pubmed.ncbi.nlm.nih.gov/6472391/',
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
name: 'Purssell RA, et al. Derivation and validation of a formula to calculate the osmolal gap. Annals of Emergency Medicine. 2001.',
|
|
10
|
+
url: 'https://pubmed.ncbi.nlm.nih.gov/11385324/',
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
name: 'MedlinePlus / NIH: Osmolality blood test',
|
|
14
|
+
url: 'https://medlineplus.gov/lab-tests/osmolality-tests/',
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
name: 'Wikipedia: Osmol gap',
|
|
18
|
+
url: 'https://en.wikipedia.org/wiki/Osmol_gap',
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
name: 'EXTRIP Workgroup: extracorporeal treatment recommendations for methanol and ethylene glycol poisoning',
|
|
22
|
+
url: 'https://www.extrip-workgroup.org/',
|
|
23
|
+
},
|
|
24
|
+
];
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
---
|
|
2
|
+
import './serum-osmolality-calculator.css';
|
|
3
|
+
import type { SerumOsmolalityCalculatorUI } from './ui';
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
locale?: string;
|
|
7
|
+
ui?: Record<string, unknown>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const t = (Astro.props.ui || {}) as SerumOsmolalityCalculatorUI;
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
<div class="osm" data-ui={JSON.stringify(t)}>
|
|
14
|
+
<div class="osm__warning" role="alert" aria-label={t.educationWarningTitle}>
|
|
15
|
+
<div class="osm__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="osm__grid">
|
|
23
|
+
<section class="osm__panel osm__panel--results">
|
|
24
|
+
<div class="osm__hero">
|
|
25
|
+
<div class="osm__metric osm__metric--primary">
|
|
26
|
+
<span>{t.calculatedLabel}</span>
|
|
27
|
+
<strong id="osm-calculated">{t.emptyValue}</strong>
|
|
28
|
+
<small>{t.mosm}</small>
|
|
29
|
+
</div>
|
|
30
|
+
<div id="osm-gap-chip" class="osm__metric osm__gap-chip">
|
|
31
|
+
<span>{t.gapLabel}</span>
|
|
32
|
+
<strong id="osm-gap">{t.emptyValue}</strong>
|
|
33
|
+
<small id="osm-gap-state">{t.gapUnavailable}</small>
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
|
|
37
|
+
<div class="osm__bars" aria-label={t.gapLabel}>
|
|
38
|
+
<div class="osm__bar-row">
|
|
39
|
+
<span>{t.calculatedShort}</span>
|
|
40
|
+
<div class="osm__bar-track"><div id="osm-calc-bar" class="osm__bar osm__bar--calculated"></div></div>
|
|
41
|
+
<b id="osm-calc-bar-value">{t.emptyValue}</b>
|
|
42
|
+
</div>
|
|
43
|
+
<div class="osm__bar-row" id="osm-measured-row">
|
|
44
|
+
<span>{t.measuredShort}</span>
|
|
45
|
+
<div class="osm__bar-track">
|
|
46
|
+
<div id="osm-measured-bar" class="osm__bar osm__bar--measured"></div>
|
|
47
|
+
<div id="osm-gap-bar" class="osm__bar-gap"></div>
|
|
48
|
+
</div>
|
|
49
|
+
<b id="osm-measured-bar-value">{t.emptyValue}</b>
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
<div id="osm-alert" class="osm__alert" hidden>
|
|
54
|
+
<strong>{t.elevatedPanelTitle}</strong>
|
|
55
|
+
<p>{t.elevatedPanelBody}</p>
|
|
56
|
+
<span>{t.elevatedCauses}</span>
|
|
57
|
+
</div>
|
|
58
|
+
</section>
|
|
59
|
+
|
|
60
|
+
<section class="osm__panel osm__panel--inputs">
|
|
61
|
+
<label class="osm__field">
|
|
62
|
+
<span>{t.sodiumLabel}</span>
|
|
63
|
+
<div class="osm__input-shell">
|
|
64
|
+
<input id="osm-na" type="number" min="0" step="0.1" placeholder={t.sodiumPlaceholder} />
|
|
65
|
+
<small>{t.sodiumUnit}</small>
|
|
66
|
+
</div>
|
|
67
|
+
</label>
|
|
68
|
+
|
|
69
|
+
<label class="osm__field">
|
|
70
|
+
<span>{t.glucoseLabel}</span>
|
|
71
|
+
<div class="osm__input-shell osm__input-shell--toggle">
|
|
72
|
+
<input id="osm-glucose" type="number" min="0" step="0.1" placeholder={t.glucosePlaceholder} />
|
|
73
|
+
<div class="osm__toggle" role="group" aria-label={t.glucoseLabel}>
|
|
74
|
+
<button type="button" class="osm__toggle-btn osm__toggle-btn--active" data-glucose-unit="mg/dL">{t.glucoseMgdl}</button>
|
|
75
|
+
<button type="button" class="osm__toggle-btn" data-glucose-unit="mmol/L">{t.glucoseMmol}</button>
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
<small class="osm__converted" id="osm-glucose-converted"></small>
|
|
79
|
+
</label>
|
|
80
|
+
|
|
81
|
+
<label class="osm__field">
|
|
82
|
+
<span>{t.nitrogenLabel}</span>
|
|
83
|
+
<div class="osm__input-shell osm__input-shell--toggle">
|
|
84
|
+
<input id="osm-nitrogen" type="number" min="0" step="0.1" placeholder={t.nitrogenPlaceholder} />
|
|
85
|
+
<div class="osm__toggle" role="group" aria-label={t.nitrogenLabel}>
|
|
86
|
+
<button type="button" class="osm__toggle-btn osm__toggle-btn--active" data-nitrogen-unit="bun-mg/dL">{t.bunMgdl}</button>
|
|
87
|
+
<button type="button" class="osm__toggle-btn" data-nitrogen-unit="urea-mmol/L">{t.ureaMmol}</button>
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
<small class="osm__converted" id="osm-nitrogen-converted"></small>
|
|
91
|
+
</label>
|
|
92
|
+
|
|
93
|
+
<label class="osm__field">
|
|
94
|
+
<span>{t.measuredLabel}</span>
|
|
95
|
+
<div class="osm__input-shell">
|
|
96
|
+
<input id="osm-measured" type="number" min="0" step="0.1" placeholder={t.measuredPlaceholder} />
|
|
97
|
+
<small>{t.mosm}</small>
|
|
98
|
+
</div>
|
|
99
|
+
<small class="osm__hint">{t.measuredHint}</small>
|
|
100
|
+
</label>
|
|
101
|
+
|
|
102
|
+
<div class="osm__formula">
|
|
103
|
+
<span>{t.formulaLabel}</span>
|
|
104
|
+
<code id="osm-formula">{t.formulaConventional}</code>
|
|
105
|
+
</div>
|
|
106
|
+
</section>
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
|
|
110
|
+
<script>
|
|
111
|
+
type GlucoseUnit = 'mg/dL' | 'mmol/L';
|
|
112
|
+
type NitrogenUnit = 'bun-mg/dL' | 'urea-mmol/L';
|
|
113
|
+
|
|
114
|
+
interface OsmCtx {
|
|
115
|
+
root: Element;
|
|
116
|
+
ui: Record<string, string>;
|
|
117
|
+
sodium: HTMLInputElement;
|
|
118
|
+
glucose: HTMLInputElement;
|
|
119
|
+
nitrogen: HTMLInputElement;
|
|
120
|
+
measured: HTMLInputElement;
|
|
121
|
+
glucoseUnit: GlucoseUnit;
|
|
122
|
+
nitrogenUnit: NitrogenUnit;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
interface OsmRenderState {
|
|
126
|
+
calculated: number;
|
|
127
|
+
measuredRaw: number;
|
|
128
|
+
gap: number | null;
|
|
129
|
+
elevated: boolean;
|
|
130
|
+
maxBar: number;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function osmQs<T extends HTMLElement | SVGElement>(root: Element, selector: string): T {
|
|
134
|
+
const el = root.querySelector<T>(selector);
|
|
135
|
+
if (!el) throw new Error(`Missing ${selector}`);
|
|
136
|
+
return el;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function glucoseToMmol(value: number, unit: GlucoseUnit): number {
|
|
140
|
+
return unit === 'mg/dL' ? value / 18 : value;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function nitrogenToUreaMmol(value: number, unit: NitrogenUnit): number {
|
|
144
|
+
return unit === 'bun-mg/dL' ? value / 2.8 : value;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function fmt(value: number): string {
|
|
148
|
+
return String(Math.round(value * 10) / 10);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function setWidth(el: HTMLElement, value: number, max: number) {
|
|
152
|
+
const pct = value > 0 ? Math.max(3, Math.min(100, (value / max) * 100)) : 0;
|
|
153
|
+
el.style.width = `${pct}%`;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function resetOsm(ctx: OsmCtx) {
|
|
157
|
+
osmQs<HTMLElement>(ctx.root, '#osm-calculated').textContent = ctx.ui.emptyValue;
|
|
158
|
+
osmQs<HTMLElement>(ctx.root, '#osm-calc-bar-value').textContent = ctx.ui.emptyValue;
|
|
159
|
+
osmQs<HTMLElement>(ctx.root, '#osm-gap').textContent = ctx.ui.emptyValue;
|
|
160
|
+
osmQs<HTMLElement>(ctx.root, '#osm-gap-state').textContent = ctx.ui.gapUnavailable;
|
|
161
|
+
osmQs<HTMLElement>(ctx.root, '#osm-measured-bar-value').textContent = ctx.ui.emptyValue;
|
|
162
|
+
osmQs<HTMLElement>(ctx.root, '#osm-alert').hidden = true;
|
|
163
|
+
osmQs<HTMLElement>(ctx.root, '#osm-gap-chip').className = 'osm__metric osm__gap-chip';
|
|
164
|
+
setWidth(osmQs<HTMLElement>(ctx.root, '#osm-calc-bar'), 0, 330);
|
|
165
|
+
setWidth(osmQs<HTMLElement>(ctx.root, '#osm-measured-bar'), 0, 330);
|
|
166
|
+
setWidth(osmQs<HTMLElement>(ctx.root, '#osm-gap-bar'), 0, 330);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function getGapState(ctx: OsmCtx, gap: number | null, elevated: boolean): string {
|
|
170
|
+
if (gap === null) return ctx.ui.gapUnavailable;
|
|
171
|
+
return elevated ? ctx.ui.elevatedGap : ctx.ui.normalGap;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function renderNumericResults(ctx: OsmCtx, state: OsmRenderState) {
|
|
175
|
+
const hasMeasured = state.measuredRaw > 0;
|
|
176
|
+
osmQs<HTMLElement>(ctx.root, '#osm-calculated').textContent = fmt(state.calculated);
|
|
177
|
+
osmQs<HTMLElement>(ctx.root, '#osm-calc-bar-value').textContent = fmt(state.calculated);
|
|
178
|
+
osmQs<HTMLElement>(ctx.root, '#osm-gap').textContent = state.gap === null ? ctx.ui.emptyValue : fmt(state.gap);
|
|
179
|
+
osmQs<HTMLElement>(ctx.root, '#osm-gap-state').textContent = getGapState(ctx, state.gap, state.elevated);
|
|
180
|
+
osmQs<HTMLElement>(ctx.root, '#osm-measured-bar-value').textContent = hasMeasured ? fmt(state.measuredRaw) : ctx.ui.emptyValue;
|
|
181
|
+
osmQs<HTMLElement>(ctx.root, '#osm-measured-row').classList.toggle('osm__bar-row--muted', !hasMeasured);
|
|
182
|
+
osmQs<HTMLElement>(ctx.root, '#osm-alert').hidden = !state.elevated;
|
|
183
|
+
osmQs<HTMLElement>(ctx.root, '#osm-gap-chip').className = state.elevated ? 'osm__metric osm__gap-chip osm__gap-chip--elevated' : 'osm__metric osm__gap-chip';
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function renderBars(ctx: OsmCtx, state: OsmRenderState) {
|
|
187
|
+
const hasMeasured = state.measuredRaw > 0;
|
|
188
|
+
setWidth(osmQs<HTMLElement>(ctx.root, '#osm-calc-bar'), state.calculated, state.maxBar);
|
|
189
|
+
setWidth(osmQs<HTMLElement>(ctx.root, '#osm-measured-bar'), hasMeasured ? state.measuredRaw : 0, state.maxBar);
|
|
190
|
+
osmQs<HTMLElement>(ctx.root, '#osm-gap-bar').style.width = state.gap !== null && state.gap > 0 ? `${Math.min(100, (state.gap / state.maxBar) * 100)}%` : '0%';
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function renderFormula(ctx: OsmCtx) {
|
|
194
|
+
const isSi = ctx.glucoseUnit === 'mmol/L' && ctx.nitrogenUnit === 'urea-mmol/L';
|
|
195
|
+
osmQs<HTMLElement>(ctx.root, '#osm-formula').textContent = isSi ? ctx.ui.formulaSi : ctx.ui.formulaConventional;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function renderConversions(ctx: OsmCtx, glucoseMmol: number, ureaMmol: number) {
|
|
199
|
+
osmQs<HTMLElement>(ctx.root, '#osm-glucose-converted').textContent =
|
|
200
|
+
ctx.glucoseUnit === 'mg/dL'
|
|
201
|
+
? `${ctx.ui.convertedGlucose}: ${fmt(glucoseMmol)} ${ctx.ui.glucoseMmol}`
|
|
202
|
+
: `${ctx.ui.convertedGlucose}: ${fmt(glucoseMmol * 18)} ${ctx.ui.glucoseMgdl}`;
|
|
203
|
+
osmQs<HTMLElement>(ctx.root, '#osm-nitrogen-converted').textContent =
|
|
204
|
+
ctx.nitrogenUnit === 'bun-mg/dL'
|
|
205
|
+
? `${ctx.ui.convertedNitrogen}: ${fmt(ureaMmol)} ${ctx.ui.ureaMmol}`
|
|
206
|
+
: `${ctx.ui.convertedNitrogen}: ${fmt(ureaMmol * 2.8)} ${ctx.ui.bunMgdl}`;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function renderOsm(ctx: OsmCtx) {
|
|
210
|
+
const sodium = parseFloat(ctx.sodium.value);
|
|
211
|
+
const glucoseRaw = parseFloat(ctx.glucose.value);
|
|
212
|
+
const nitrogenRaw = parseFloat(ctx.nitrogen.value);
|
|
213
|
+
const measuredRaw = parseFloat(ctx.measured.value);
|
|
214
|
+
const hasCore = sodium > 0 && glucoseRaw >= 0 && nitrogenRaw >= 0;
|
|
215
|
+
|
|
216
|
+
if (!hasCore) {
|
|
217
|
+
resetOsm(ctx);
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const glucoseMmol = glucoseToMmol(glucoseRaw, ctx.glucoseUnit);
|
|
222
|
+
const ureaMmol = nitrogenToUreaMmol(nitrogenRaw, ctx.nitrogenUnit);
|
|
223
|
+
const calculated = Math.round((2 * sodium + glucoseMmol + ureaMmol) * 10) / 10;
|
|
224
|
+
const hasMeasured = measuredRaw > 0;
|
|
225
|
+
const gap = hasMeasured ? Math.round((measuredRaw - calculated) * 10) / 10 : null;
|
|
226
|
+
const elevated = gap !== null && gap > 10;
|
|
227
|
+
const maxBar = Math.max(330, calculated, hasMeasured ? measuredRaw : 0);
|
|
228
|
+
|
|
229
|
+
const state = { calculated, measuredRaw, gap, elevated, maxBar };
|
|
230
|
+
renderNumericResults(ctx, state);
|
|
231
|
+
renderBars(ctx, state);
|
|
232
|
+
renderFormula(ctx);
|
|
233
|
+
renderConversions(ctx, glucoseMmol, ureaMmol);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function initOsm(root: Element) {
|
|
237
|
+
const ctx: OsmCtx = {
|
|
238
|
+
root,
|
|
239
|
+
ui: JSON.parse((root as HTMLElement).dataset.ui || '{}') as Record<string, string>,
|
|
240
|
+
sodium: osmQs<HTMLInputElement>(root, '#osm-na'),
|
|
241
|
+
glucose: osmQs<HTMLInputElement>(root, '#osm-glucose'),
|
|
242
|
+
nitrogen: osmQs<HTMLInputElement>(root, '#osm-nitrogen'),
|
|
243
|
+
measured: osmQs<HTMLInputElement>(root, '#osm-measured'),
|
|
244
|
+
glucoseUnit: 'mg/dL',
|
|
245
|
+
nitrogenUnit: 'bun-mg/dL',
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
root.querySelectorAll<HTMLButtonElement>('[data-glucose-unit]').forEach((btn) => {
|
|
249
|
+
btn.addEventListener('click', () => {
|
|
250
|
+
ctx.glucoseUnit = btn.dataset.glucoseUnit as GlucoseUnit;
|
|
251
|
+
root.querySelectorAll<HTMLButtonElement>('[data-glucose-unit]').forEach((item) => item.classList.toggle('osm__toggle-btn--active', item === btn));
|
|
252
|
+
renderOsm(ctx);
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
root.querySelectorAll<HTMLButtonElement>('[data-nitrogen-unit]').forEach((btn) => {
|
|
257
|
+
btn.addEventListener('click', () => {
|
|
258
|
+
ctx.nitrogenUnit = btn.dataset.nitrogenUnit as NitrogenUnit;
|
|
259
|
+
root.querySelectorAll<HTMLButtonElement>('[data-nitrogen-unit]').forEach((item) => item.classList.toggle('osm__toggle-btn--active', item === btn));
|
|
260
|
+
renderOsm(ctx);
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
[ctx.sodium, ctx.glucose, ctx.nitrogen, ctx.measured].forEach((input) => input.addEventListener('input', () => renderOsm(ctx)));
|
|
265
|
+
renderOsm(ctx);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
document.querySelectorAll('.osm').forEach(initOsm);
|
|
269
|
+
</script>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { HealthToolEntry } from '../../types';
|
|
2
|
+
import type { SerumOsmolalityCalculatorUI } from './ui';
|
|
3
|
+
|
|
4
|
+
export const serumOsmolalityCalculator: HealthToolEntry<SerumOsmolalityCalculatorUI> = {
|
|
5
|
+
id: 'serumOsmolalityCalculator',
|
|
6
|
+
icons: {
|
|
7
|
+
bg: 'mdi:flask-outline',
|
|
8
|
+
fg: 'mdi:chart-bar-stacked',
|
|
9
|
+
},
|
|
10
|
+
i18n: {
|
|
11
|
+
en: () => import('./i18n/en').then((m) => m.content),
|
|
12
|
+
de: () => import('./i18n/de').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
|
+
};
|