@jjlmoya/utils-health 1.30.0 → 1.31.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/correctedSodiumHyperglycemiaCalculator/bibliography.astro +14 -0
  8. package/src/tool/correctedSodiumHyperglycemiaCalculator/bibliography.ts +24 -0
  9. package/src/tool/correctedSodiumHyperglycemiaCalculator/component.astro +229 -0
  10. package/src/tool/correctedSodiumHyperglycemiaCalculator/corrected-sodium-hyperglycemia-calculator.css +474 -0
  11. package/src/tool/correctedSodiumHyperglycemiaCalculator/entry.ts +27 -0
  12. package/src/tool/correctedSodiumHyperglycemiaCalculator/i18n/de.ts +285 -0
  13. package/src/tool/correctedSodiumHyperglycemiaCalculator/i18n/en.ts +285 -0
  14. package/src/tool/correctedSodiumHyperglycemiaCalculator/i18n/es.ts +285 -0
  15. package/src/tool/correctedSodiumHyperglycemiaCalculator/i18n/fr.ts +285 -0
  16. package/src/tool/correctedSodiumHyperglycemiaCalculator/i18n/id.ts +285 -0
  17. package/src/tool/correctedSodiumHyperglycemiaCalculator/i18n/it.ts +285 -0
  18. package/src/tool/correctedSodiumHyperglycemiaCalculator/i18n/ja.ts +285 -0
  19. package/src/tool/correctedSodiumHyperglycemiaCalculator/i18n/ko.ts +285 -0
  20. package/src/tool/correctedSodiumHyperglycemiaCalculator/i18n/nl.ts +285 -0
  21. package/src/tool/correctedSodiumHyperglycemiaCalculator/i18n/pl.ts +285 -0
  22. package/src/tool/correctedSodiumHyperglycemiaCalculator/i18n/pt.ts +285 -0
  23. package/src/tool/correctedSodiumHyperglycemiaCalculator/i18n/ru.ts +285 -0
  24. package/src/tool/correctedSodiumHyperglycemiaCalculator/i18n/sv.ts +285 -0
  25. package/src/tool/correctedSodiumHyperglycemiaCalculator/i18n/tr.ts +285 -0
  26. package/src/tool/correctedSodiumHyperglycemiaCalculator/i18n/zh.ts +285 -0
  27. package/src/tool/correctedSodiumHyperglycemiaCalculator/index.ts +12 -0
  28. package/src/tool/correctedSodiumHyperglycemiaCalculator/logic.ts +51 -0
  29. package/src/tool/correctedSodiumHyperglycemiaCalculator/seo.astro +15 -0
  30. package/src/tool/correctedSodiumHyperglycemiaCalculator/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.30.0",
3
+ "version": "1.31.0",
4
4
  "type": "module",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -23,13 +23,14 @@ import { leanBodyMassCalculator } from '../tool/leanBodyMassCalculator/entry';
23
23
  import { metabolicAgeCalculator } from '../tool/metabolicAgeCalculator/entry';
24
24
  import { bunCreatinineCalculator } from '../tool/bunCreatinineCalculator/entry';
25
25
  import { serumOsmolalityCalculator } from '../tool/serumOsmolalityCalculator/entry';
26
+ import { correctedSodiumHyperglycemiaCalculator } from '../tool/correctedSodiumHyperglycemiaCalculator/entry';
26
27
  import { fractionalExcretionSodiumCalculator } from '../tool/fractionalExcretionSodiumCalculator/entry';
27
28
  import { targetHeightEstimator } from '../tool/targetHeightEstimator/entry';
28
29
  import { correctedReticulocyteIndex } from '../tool/correctedReticulocyteIndex/entry';
29
30
 
30
31
  export const healthCategory: HealthCategoryEntry = {
31
32
  icon: 'mdi:heart-pulse',
32
- tools: [bmiCalculator, amputationAdjustedBmiCalculator, 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],
33
+ tools: [bmiCalculator, amputationAdjustedBmiCalculator, bodyAdiposityIndexCalculator, bodyFatPercentageCalculator, waistHipRatioCalculator, waistHeightRatioCalculator, leanBodyMassCalculator, metabolicAgeCalculator, caloricDeficitCalculator, dailyProteinCalculator, macronutrientCalculator, tdeeCalculator, bunCreatinineCalculator, serumOsmolalityCalculator, correctedSodiumHyperglycemiaCalculator, fractionalExcretionSodiumCalculator, correctedReticulocyteIndex, targetHeightEstimator, daltonismSimulator, tinnitusReliever, breathingVisualizer, caffeineTracker, waterPurifier, digestionStopwatch, binauralTuner, pelliRobsonTest, peripheralVisionTrainer, epworthSleepinessScale],
33
34
  i18n: {
34
35
  en: () => import('./i18n/en').then((m) => m.content),
35
36
  es: () => import('./i18n/es').then((m) => m.content),
package/src/entries.ts CHANGED
@@ -20,6 +20,7 @@ export { macronutrientCalculator } from './tool/macronutrientCalculator/entry';
20
20
  export { tdeeCalculator } from './tool/tdeeCalculator/entry';
21
21
  export { bunCreatinineCalculator } from './tool/bunCreatinineCalculator/entry';
22
22
  export { serumOsmolalityCalculator } from './tool/serumOsmolalityCalculator/entry';
23
+ export { correctedSodiumHyperglycemiaCalculator } from './tool/correctedSodiumHyperglycemiaCalculator/entry';
23
24
  export { fractionalExcretionSodiumCalculator } from './tool/fractionalExcretionSodiumCalculator/entry';
24
25
  export { targetHeightEstimator } from './tool/targetHeightEstimator/entry';
25
26
  export { correctedReticulocyteIndex } from './tool/correctedReticulocyteIndex/entry';
@@ -53,6 +54,7 @@ import { macronutrientCalculator } from './tool/macronutrientCalculator/entry';
53
54
  import { tdeeCalculator } from './tool/tdeeCalculator/entry';
54
55
  import { bunCreatinineCalculator } from './tool/bunCreatinineCalculator/entry';
55
56
  import { serumOsmolalityCalculator } from './tool/serumOsmolalityCalculator/entry';
57
+ import { correctedSodiumHyperglycemiaCalculator } from './tool/correctedSodiumHyperglycemiaCalculator/entry';
56
58
  import { fractionalExcretionSodiumCalculator } from './tool/fractionalExcretionSodiumCalculator/entry';
57
59
  import { targetHeightEstimator } from './tool/targetHeightEstimator/entry';
58
60
  import { correctedReticulocyteIndex } from './tool/correctedReticulocyteIndex/entry';
@@ -63,4 +65,4 @@ import { screenDecompressionTime } from './tool/screenDecompressionTime/entry';
63
65
  import { tinnitusReliever } from './tool/tinnitusReliever/entry';
64
66
  import { ubeCalculator } from './tool/ubeCalculator/entry';
65
67
  import { waterPurifier } from './tool/waterPurifier/entry';
66
- export const ALL_ENTRIES = [binauralTuner, bloodUnitConverter, bmiCalculator, amputationAdjustedBmiCalculator, 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];
68
+ export const ALL_ENTRIES = [binauralTuner, bloodUnitConverter, bmiCalculator, amputationAdjustedBmiCalculator, bodyAdiposityIndexCalculator, bodyFatPercentageCalculator, waistHipRatioCalculator, waistHeightRatioCalculator, breathingVisualizer, caffeineTracker, caloricDeficitCalculator, dailyProteinCalculator, macronutrientCalculator, tdeeCalculator, bunCreatinineCalculator, serumOsmolalityCalculator, correctedSodiumHyperglycemiaCalculator, fractionalExcretionSodiumCalculator, correctedReticulocyteIndex, targetHeightEstimator, daltonismSimulator, digestionStopwatch, epworthSleepinessScale, hydrationCalculator, leanBodyMassCalculator, metabolicAgeCalculator, pelliRobsonTest, peripheralVisionTrainer, readingDistanceCalculator, screenDecompressionTime, tinnitusReliever, ubeCalculator, waterPurifier];
package/src/index.ts CHANGED
@@ -27,6 +27,7 @@ export * from './tool/macronutrientCalculator';
27
27
  export * from './tool/tdeeCalculator';
28
28
  export * from './tool/bunCreatinineCalculator';
29
29
  export * from './tool/serumOsmolalityCalculator';
30
+ export * from './tool/correctedSodiumHyperglycemiaCalculator';
30
31
  export * from './tool/fractionalExcretionSodiumCalculator';
31
32
  export * from './tool/targetHeightEstimator';
32
33
  export * from './tool/correctedReticulocyteIndex';
@@ -36,6 +36,6 @@ describe('Locale Completeness Validation', () => {
36
36
  });
37
37
 
38
38
  it('all tools registered', () => {
39
- expect(ALL_TOOLS.length).toBe(32);
39
+ expect(ALL_TOOLS.length).toBe(33);
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 30 tools in ALL_TOOLS', () => {
8
- expect(ALL_TOOLS.length).toBe(32);
7
+ it('should have 33 tools in ALL_TOOLS', () => {
8
+ expect(ALL_TOOLS.length).toBe(33);
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 { correctedSodiumHyperglycemiaCalculator } 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 correctedSodiumHyperglycemiaCalculator.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: 'Katz MA. Hyperglycemia-induced hyponatremia: calculation of expected serum sodium depression. New England Journal of Medicine. 1973.',
6
+ url: 'https://pubmed.ncbi.nlm.nih.gov/4700474/',
7
+ },
8
+ {
9
+ name: 'Hillier TA, Abbott RD, Barrett EJ. Hyponatremia: evaluating the correction factor for hyperglycemia. American Journal of Medicine. 1999.',
10
+ url: 'https://pubmed.ncbi.nlm.nih.gov/10225241/',
11
+ },
12
+ {
13
+ name: 'Joint British Diabetes Societies guideline: management of diabetic ketoacidosis in adults',
14
+ url: 'https://abcd.care/resource/jbds-02-management-diabetic-ketoacidosis-adults',
15
+ },
16
+ {
17
+ name: 'American Diabetes Association. Hyperglycemic crises in adult patients with diabetes.',
18
+ url: 'https://pubmed.ncbi.nlm.nih.gov/19564476/',
19
+ },
20
+ {
21
+ name: 'Merck Manual Professional: Hyponatremia',
22
+ url: 'https://www.merckmanuals.com/professional/endocrine-and-metabolic-disorders/electrolyte-disorders/hyponatremia',
23
+ },
24
+ ];
@@ -0,0 +1,229 @@
1
+ ---
2
+ import './corrected-sodium-hyperglycemia-calculator.css';
3
+ import type { CorrectedSodiumHyperglycemiaCalculatorUI } from './ui';
4
+
5
+ interface Props {
6
+ locale?: string;
7
+ ui?: Record<string, unknown>;
8
+ }
9
+
10
+ const t = (Astro.props.ui || {}) as CorrectedSodiumHyperglycemiaCalculatorUI;
11
+ ---
12
+
13
+ <div class="csh" data-ui={JSON.stringify(t)}>
14
+ <div class="csh__warning" role="alert" aria-label={t.safetyTitle}>
15
+ <div class="csh__warning-mark">{t.safetyMark}</div>
16
+ <div>
17
+ <strong>{t.safetyTitle}</strong>
18
+ <span>{t.safetyBody}</span>
19
+ </div>
20
+ </div>
21
+
22
+ <div class="csh__layout">
23
+ <section class="csh__rule" aria-label={t.glucoseAxisLabel}>
24
+ <div class="csh__glucose-rail">
25
+ <span>{t.glucoseTickLow}</span>
26
+ <span>{t.glucoseTickMid}</span>
27
+ <span>{t.glucoseTickHigh}</span>
28
+ <div id="csh-glucose-fill" class="csh__glucose-fill"></div>
29
+ <div id="csh-glucose-dot" class="csh__glucose-dot"></div>
30
+ </div>
31
+ <div class="csh__sodium-stage" aria-label={t.sodiumAxisLabel}>
32
+ <div class="csh__sodium-band csh__sodium-band--low"></div>
33
+ <div class="csh__sodium-band csh__sodium-band--normal"></div>
34
+ <div class="csh__sodium-band csh__sodium-band--high"></div>
35
+ <div id="csh-measured-pin" class="csh__pin csh__pin--measured">
36
+ <b>{t.measuredMarker}</b>
37
+ <span id="csh-measured-pin-value">{t.emptyValue}</span>
38
+ </div>
39
+ <div id="csh-corrected-pin" class="csh__pin csh__pin--corrected">
40
+ <b>{t.correctedMarker}</b>
41
+ <span id="csh-corrected-pin-value">{t.emptyValue}</span>
42
+ </div>
43
+ </div>
44
+ </section>
45
+
46
+ <section class="csh__panel">
47
+ <div class="csh__inputs">
48
+ <label class="csh__field">
49
+ <span>{t.sodiumLabel}</span>
50
+ <div class="csh__input-shell">
51
+ <input id="csh-sodium" type="number" min="80" max="190" step="0.1" placeholder={t.sodiumPlaceholder} />
52
+ <small>{t.sodiumUnit}</small>
53
+ </div>
54
+ </label>
55
+
56
+ <label class="csh__field">
57
+ <span>{t.glucoseLabel}</span>
58
+ <div class="csh__input-shell csh__input-shell--toggle">
59
+ <input id="csh-glucose" type="number" min="0" step="0.1" placeholder={t.glucosePlaceholder} />
60
+ <div class="csh__toggle" role="group" aria-label={t.glucoseLabel}>
61
+ <button type="button" class="csh__toggle-btn csh__toggle-btn--active" data-glucose-unit="mg/dL">{t.glucoseMgdl}</button>
62
+ <button type="button" class="csh__toggle-btn" data-glucose-unit="mmol/L">{t.glucoseMmol}</button>
63
+ </div>
64
+ </div>
65
+ <small id="csh-converted" class="csh__converted"></small>
66
+ </label>
67
+ </div>
68
+
69
+ <div class="csh__factor" role="group" aria-label={t.factorLabel}>
70
+ <button type="button" class="csh__factor-btn csh__factor-btn--active" data-mode="katz">
71
+ <strong>{t.katzLabel}</strong>
72
+ <span>{t.katzHint}</span>
73
+ </button>
74
+ <button type="button" class="csh__factor-btn" data-mode="hillier">
75
+ <strong>{t.hillierLabel}</strong>
76
+ <span>{t.hillierHint}</span>
77
+ </button>
78
+ </div>
79
+
80
+ <div class="csh__results">
81
+ <div class="csh__result csh__result--main">
82
+ <span>{t.correctedLabel}</span>
83
+ <strong id="csh-corrected">{t.emptyValue}</strong>
84
+ <small>{t.sodiumUnit}</small>
85
+ </div>
86
+ <div class="csh__result">
87
+ <span>{t.correctionLabel}</span>
88
+ <strong id="csh-correction">{t.emptyValue}</strong>
89
+ <small>{t.sodiumUnit}</small>
90
+ </div>
91
+ <div class="csh__result">
92
+ <span>{t.factorUsedLabel}</span>
93
+ <strong id="csh-factor">{t.emptyValue}</strong>
94
+ <small>{t.factorUnitLabel}</small>
95
+ </div>
96
+ </div>
97
+
98
+ <div class="csh__formula">
99
+ <span>{t.formulaLabel}</span>
100
+ <code id="csh-formula">{t.formulaKatz}</code>
101
+ </div>
102
+
103
+ <div id="csh-interpretation" class="csh__interpretation">
104
+ <strong>{t.interpretationNormal}</strong>
105
+ <span>{t.interpretationBody}</span>
106
+ </div>
107
+ </section>
108
+ </div>
109
+ </div>
110
+
111
+ <script>
112
+ type GlucoseUnit = 'mg/dL' | 'mmol/L';
113
+ type Mode = 'katz' | 'hillier';
114
+
115
+ interface Ctx {
116
+ root: Element;
117
+ ui: Record<string, string>;
118
+ sodium: HTMLInputElement;
119
+ glucose: HTMLInputElement;
120
+ glucoseUnit: GlucoseUnit;
121
+ mode: Mode;
122
+ }
123
+
124
+ function qs<T extends HTMLElement>(root: Element, selector: string): T {
125
+ const el = root.querySelector<T>(selector);
126
+ if (!el) throw new Error(`Missing ${selector}`);
127
+ return el;
128
+ }
129
+
130
+ function fmt(value: number): string {
131
+ return String(Math.round(value * 10) / 10);
132
+ }
133
+
134
+ function glucoseToMgdl(value: number, unit: GlucoseUnit): number {
135
+ return unit === 'mmol/L' ? value * 18.0182 : value;
136
+ }
137
+
138
+ function factorFor(glucoseMgdl: number, mode: Mode): 1.6 | 2.4 {
139
+ return mode === 'hillier' && glucoseMgdl > 400 ? 2.4 : 1.6;
140
+ }
141
+
142
+ function sodiumPosition(value: number): number {
143
+ return Math.max(12, Math.min(88, ((value - 115) / 45) * 100));
144
+ }
145
+
146
+ function glucosePosition(value: number): number {
147
+ return Math.max(0, Math.min(100, ((value - 60) / 740) * 100));
148
+ }
149
+
150
+ function getCorrectedLabel(ctx: Ctx, corrected: number): string {
151
+ if (corrected < 135) return ctx.ui.interpretationLow;
152
+ if (corrected > 145) return ctx.ui.interpretationHigh;
153
+ return ctx.ui.interpretationNormal;
154
+ }
155
+
156
+ function reset(ctx: Ctx) {
157
+ ['#csh-corrected', '#csh-correction', '#csh-factor', '#csh-measured-pin-value', '#csh-corrected-pin-value'].forEach((selector) => {
158
+ qs<HTMLElement>(ctx.root, selector).textContent = ctx.ui.emptyValue;
159
+ });
160
+ qs<HTMLElement>(ctx.root, '#csh-glucose-fill').style.height = '0%';
161
+ qs<HTMLElement>(ctx.root, '#csh-glucose-dot').style.bottom = '0%';
162
+ qs<HTMLElement>(ctx.root, '#csh-measured-pin').style.left = '12%';
163
+ qs<HTMLElement>(ctx.root, '#csh-corrected-pin').style.left = '12%';
164
+ }
165
+
166
+ function render(ctx: Ctx) {
167
+ const sodium = parseFloat(ctx.sodium.value);
168
+ const glucoseRaw = parseFloat(ctx.glucose.value);
169
+ if (!(sodium > 0) || !(glucoseRaw >= 0)) {
170
+ reset(ctx);
171
+ return;
172
+ }
173
+
174
+ const glucoseMgdl = glucoseToMgdl(glucoseRaw, ctx.glucoseUnit);
175
+ const factor = factorFor(glucoseMgdl, ctx.mode);
176
+ const correction = (factor / 100) * (glucoseMgdl - 100);
177
+ const corrected = sodium + correction;
178
+ const correctedLabel = getCorrectedLabel(ctx, corrected);
179
+
180
+ qs<HTMLElement>(ctx.root, '#csh-corrected').textContent = fmt(corrected);
181
+ qs<HTMLElement>(ctx.root, '#csh-correction').textContent = fmt(correction);
182
+ qs<HTMLElement>(ctx.root, '#csh-factor').textContent = fmt(factor);
183
+ qs<HTMLElement>(ctx.root, '#csh-formula').textContent = factor === 2.4 ? ctx.ui.formulaHillier : ctx.ui.formulaKatz;
184
+ qs<HTMLElement>(ctx.root, '#csh-measured-pin-value').textContent = fmt(sodium);
185
+ qs<HTMLElement>(ctx.root, '#csh-corrected-pin-value').textContent = fmt(corrected);
186
+ qs<HTMLElement>(ctx.root, '#csh-interpretation').querySelector('strong')!.textContent = correctedLabel;
187
+
188
+ qs<HTMLElement>(ctx.root, '#csh-glucose-fill').style.height = `${glucosePosition(glucoseMgdl)}%`;
189
+ qs<HTMLElement>(ctx.root, '#csh-glucose-dot').style.bottom = `${glucosePosition(glucoseMgdl)}%`;
190
+ qs<HTMLElement>(ctx.root, '#csh-measured-pin').style.left = `${sodiumPosition(sodium)}%`;
191
+ qs<HTMLElement>(ctx.root, '#csh-corrected-pin').style.left = `${sodiumPosition(corrected)}%`;
192
+ qs<HTMLElement>(ctx.root, '#csh-converted').textContent =
193
+ ctx.glucoseUnit === 'mg/dL'
194
+ ? `${ctx.ui.convertedGlucose}: ${fmt(glucoseMgdl / 18.0182)} ${ctx.ui.glucoseMmol}`
195
+ : `${ctx.ui.convertedGlucose}: ${fmt(glucoseMgdl)} ${ctx.ui.glucoseMgdl}`;
196
+ }
197
+
198
+ function init(root: Element) {
199
+ const ctx: Ctx = {
200
+ root,
201
+ ui: JSON.parse((root as HTMLElement).dataset.ui || '{}') as Record<string, string>,
202
+ sodium: qs<HTMLInputElement>(root, '#csh-sodium'),
203
+ glucose: qs<HTMLInputElement>(root, '#csh-glucose'),
204
+ glucoseUnit: 'mg/dL',
205
+ mode: 'katz',
206
+ };
207
+
208
+ root.querySelectorAll<HTMLButtonElement>('[data-glucose-unit]').forEach((btn) => {
209
+ btn.addEventListener('click', () => {
210
+ ctx.glucoseUnit = btn.dataset.glucoseUnit as GlucoseUnit;
211
+ root.querySelectorAll<HTMLButtonElement>('[data-glucose-unit]').forEach((item) => item.classList.toggle('csh__toggle-btn--active', item === btn));
212
+ render(ctx);
213
+ });
214
+ });
215
+
216
+ root.querySelectorAll<HTMLButtonElement>('[data-mode]').forEach((btn) => {
217
+ btn.addEventListener('click', () => {
218
+ ctx.mode = btn.dataset.mode as Mode;
219
+ root.querySelectorAll<HTMLButtonElement>('[data-mode]').forEach((item) => item.classList.toggle('csh__factor-btn--active', item === btn));
220
+ render(ctx);
221
+ });
222
+ });
223
+
224
+ [ctx.sodium, ctx.glucose].forEach((input) => input.addEventListener('input', () => render(ctx)));
225
+ render(ctx);
226
+ }
227
+
228
+ document.querySelectorAll('.csh').forEach(init);
229
+ </script>