@jjlmoya/utils-health 1.18.0 → 1.19.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 (32) hide show
  1. package/package.json +1 -1
  2. package/src/category/index.ts +2 -1
  3. package/src/data.ts +1 -0
  4. package/src/entries.ts +3 -1
  5. package/src/index.ts +1 -0
  6. package/src/tests/locale_completeness.test.ts +1 -1
  7. package/src/tests/tool_validation.test.ts +1 -1
  8. package/src/tool/metabolicAgeCalculator/bibliography.astro +14 -0
  9. package/src/tool/metabolicAgeCalculator/bibliography.ts +28 -0
  10. package/src/tool/metabolicAgeCalculator/component.astro +242 -0
  11. package/src/tool/metabolicAgeCalculator/entry.ts +27 -0
  12. package/src/tool/metabolicAgeCalculator/i18n/de.ts +319 -0
  13. package/src/tool/metabolicAgeCalculator/i18n/en.ts +319 -0
  14. package/src/tool/metabolicAgeCalculator/i18n/es.ts +319 -0
  15. package/src/tool/metabolicAgeCalculator/i18n/fr.ts +319 -0
  16. package/src/tool/metabolicAgeCalculator/i18n/id.ts +319 -0
  17. package/src/tool/metabolicAgeCalculator/i18n/it.ts +319 -0
  18. package/src/tool/metabolicAgeCalculator/i18n/ja.ts +319 -0
  19. package/src/tool/metabolicAgeCalculator/i18n/ko.ts +319 -0
  20. package/src/tool/metabolicAgeCalculator/i18n/nl.ts +319 -0
  21. package/src/tool/metabolicAgeCalculator/i18n/pl.ts +319 -0
  22. package/src/tool/metabolicAgeCalculator/i18n/pt.ts +319 -0
  23. package/src/tool/metabolicAgeCalculator/i18n/ru.ts +319 -0
  24. package/src/tool/metabolicAgeCalculator/i18n/sv.ts +319 -0
  25. package/src/tool/metabolicAgeCalculator/i18n/tr.ts +319 -0
  26. package/src/tool/metabolicAgeCalculator/i18n/zh.ts +319 -0
  27. package/src/tool/metabolicAgeCalculator/index.ts +12 -0
  28. package/src/tool/metabolicAgeCalculator/logic.ts +120 -0
  29. package/src/tool/metabolicAgeCalculator/metabolic-age-calculator.css +393 -0
  30. package/src/tool/metabolicAgeCalculator/seo.astro +15 -0
  31. package/src/tool/metabolicAgeCalculator/ui.ts +49 -0
  32. 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.18.0",
3
+ "version": "1.19.0",
4
4
  "type": "module",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -12,10 +12,11 @@ import { pelliRobsonTest } from '../tool/pelliRobsonTest/entry';
12
12
  import { peripheralVisionTrainer } from '../tool/peripheralVisionTrainer/entry';
13
13
  import { epworthSleepinessScale } from '../tool/epworthSleepinessScale/entry';
14
14
  import { leanBodyMassCalculator } from '../tool/leanBodyMassCalculator/entry';
15
+ import { metabolicAgeCalculator } from '../tool/metabolicAgeCalculator/entry';
15
16
 
16
17
  export const healthCategory: HealthCategoryEntry = {
17
18
  icon: 'mdi:heart-pulse',
18
- tools: [bmiCalculator, bodyAdiposityIndexCalculator, leanBodyMassCalculator, daltonismSimulator, tinnitusReliever, breathingVisualizer, caffeineTracker, waterPurifier, digestionStopwatch, binauralTuner, pelliRobsonTest, peripheralVisionTrainer, epworthSleepinessScale],
19
+ tools: [bmiCalculator, bodyAdiposityIndexCalculator, leanBodyMassCalculator, metabolicAgeCalculator, daltonismSimulator, tinnitusReliever, breathingVisualizer, caffeineTracker, waterPurifier, digestionStopwatch, binauralTuner, pelliRobsonTest, peripheralVisionTrainer, epworthSleepinessScale],
19
20
  i18n: {
20
21
  en: () => import('./i18n/en').then((m) => m.content),
21
22
  es: () => import('./i18n/es').then((m) => m.content),
package/src/data.ts CHANGED
@@ -15,6 +15,7 @@ export { bloodUnitConverter } from './tool/bloodUnitConverter';
15
15
  export { readingDistanceCalculator } from './tool/readingDistanceCalculator';
16
16
  export { hydrationCalculator } from './tool/hydrationCalculator';
17
17
  export { leanBodyMassCalculator } from './tool/leanBodyMassCalculator';
18
+ export { metabolicAgeCalculator } from './tool/metabolicAgeCalculator';
18
19
 
19
20
  export { healthCategory } from './category';
20
21
 
package/src/entries.ts CHANGED
@@ -9,6 +9,7 @@ export { digestionStopwatch } from './tool/digestionStopwatch/entry';
9
9
  export { epworthSleepinessScale } from './tool/epworthSleepinessScale/entry';
10
10
  export { hydrationCalculator } from './tool/hydrationCalculator/entry';
11
11
  export { leanBodyMassCalculator } from './tool/leanBodyMassCalculator/entry';
12
+ export { metabolicAgeCalculator } from './tool/metabolicAgeCalculator/entry';
12
13
  export { pelliRobsonTest } from './tool/pelliRobsonTest/entry';
13
14
  export { peripheralVisionTrainer } from './tool/peripheralVisionTrainer/entry';
14
15
  export { readingDistanceCalculator } from './tool/readingDistanceCalculator/entry';
@@ -28,6 +29,7 @@ import { digestionStopwatch } from './tool/digestionStopwatch/entry';
28
29
  import { epworthSleepinessScale } from './tool/epworthSleepinessScale/entry';
29
30
  import { hydrationCalculator } from './tool/hydrationCalculator/entry';
30
31
  import { leanBodyMassCalculator } from './tool/leanBodyMassCalculator/entry';
32
+ import { metabolicAgeCalculator } from './tool/metabolicAgeCalculator/entry';
31
33
  import { pelliRobsonTest } from './tool/pelliRobsonTest/entry';
32
34
  import { peripheralVisionTrainer } from './tool/peripheralVisionTrainer/entry';
33
35
  import { readingDistanceCalculator } from './tool/readingDistanceCalculator/entry';
@@ -35,4 +37,4 @@ import { screenDecompressionTime } from './tool/screenDecompressionTime/entry';
35
37
  import { tinnitusReliever } from './tool/tinnitusReliever/entry';
36
38
  import { ubeCalculator } from './tool/ubeCalculator/entry';
37
39
  import { waterPurifier } from './tool/waterPurifier/entry';
38
- export const ALL_ENTRIES = [binauralTuner, bloodUnitConverter, bmiCalculator, bodyAdiposityIndexCalculator, breathingVisualizer, caffeineTracker, daltonismSimulator, digestionStopwatch, epworthSleepinessScale, hydrationCalculator, leanBodyMassCalculator, pelliRobsonTest, peripheralVisionTrainer, readingDistanceCalculator, screenDecompressionTime, tinnitusReliever, ubeCalculator, waterPurifier];
40
+ export const ALL_ENTRIES = [binauralTuner, bloodUnitConverter, bmiCalculator, bodyAdiposityIndexCalculator, breathingVisualizer, caffeineTracker, daltonismSimulator, digestionStopwatch, epworthSleepinessScale, hydrationCalculator, leanBodyMassCalculator, metabolicAgeCalculator, pelliRobsonTest, peripheralVisionTrainer, readingDistanceCalculator, screenDecompressionTime, tinnitusReliever, ubeCalculator, waterPurifier];
package/src/index.ts CHANGED
@@ -15,6 +15,7 @@ export * from './tool/bloodUnitConverter';
15
15
  export * from './tool/readingDistanceCalculator';
16
16
  export * from './tool/hydrationCalculator';
17
17
  export * from './tool/bodyAdiposityIndexCalculator';
18
+ export * from './tool/metabolicAgeCalculator';
18
19
 
19
20
  export { healthCategory } from './category';
20
21
  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 18 tools registered', () => {
39
- expect(ALL_TOOLS.length).toBe(18);
39
+ expect(ALL_TOOLS.length).toBe(19);
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 18 tools in ALL_TOOLS', () => {
8
- expect(ALL_TOOLS.length).toBe(18);
8
+ expect(ALL_TOOLS.length).toBe(19);
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 { metabolicAgeCalculator } 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 metabolicAgeCalculator.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: 'Mifflin MD, St Jeor ST, et al.: A new predictive equation for resting energy expenditure in healthy individuals',
6
+ url: 'https://pubmed.ncbi.nlm.nih.gov/2305711/',
7
+ },
8
+ {
9
+ name: 'Harris JA, Benedict FG.: A biometric study of human basal metabolism in man',
10
+ url: 'https://doi.org/10.1073/pnas.4.12.370',
11
+ },
12
+ {
13
+ name: 'Katch FI, McArdle WD.: Nutrition, Weight Control, and Exercise',
14
+ url: 'https://archive.org/details/nutritionweightc0000katc',
15
+ },
16
+ {
17
+ name: 'National Academies: Dietary Reference Intakes for Energy',
18
+ url: 'https://www.ncbi.nlm.nih.gov/books/NBK56068/',
19
+ },
20
+ {
21
+ name: 'CDC: Physical Activity Basics',
22
+ url: 'https://www.cdc.gov/physical-activity-basics/index.html',
23
+ },
24
+ {
25
+ name: 'Wikipedia: Basal metabolic rate',
26
+ url: 'https://en.wikipedia.org/wiki/Basal_metabolic_rate',
27
+ },
28
+ ];
@@ -0,0 +1,242 @@
1
+ ---
2
+ import { Icon } from 'astro-icon/components';
3
+ import type { MetabolicAgeCalculatorUI } from './ui';
4
+
5
+ interface Props {
6
+ locale?: string;
7
+ ui?: Record<string, unknown>;
8
+ }
9
+
10
+ const t = (Astro.props.ui || {}) as MetabolicAgeCalculatorUI;
11
+ ---
12
+
13
+ <div class="mac" data-ui={JSON.stringify(t)}>
14
+ <div class="mac__grid">
15
+ <section class="mac__dial-panel" aria-live="polite">
16
+ <div class="mac__dial">
17
+ <div class="mac__arc"></div>
18
+ <div class="mac__chrono-line" title={t.chronologicalAgeLabel}></div>
19
+ <div class="mac__needle"></div>
20
+ <div class="mac__hub"></div>
21
+ <div class="mac__readout">
22
+ <span>{t.metabolicAgeLabel}</span>
23
+ <strong id="mac-metabolic-age">42</strong>
24
+ <em id="mac-delta">{t.deltaAligned}</em>
25
+ </div>
26
+ </div>
27
+ <div class="mac__dial-legend" aria-label={t.dialLegendLabel}>
28
+ <span><i class="mac__key mac__key--needle"></i>{t.metabolicAgeLabel}</span>
29
+ <span><i class="mac__key mac__key--chrono"></i>{t.chronologicalAgeLabel}</span>
30
+ </div>
31
+
32
+ <div class="mac__scale">
33
+ <div class="mac__scale-bar"><span id="mac-score-bar" style="width: 62%"></span></div>
34
+ <div class="mac__scale-labels">
35
+ <strong id="mac-category">{t.categoryBalanced}</strong>
36
+ <span>{t.scaleRangeLabel}</span>
37
+ </div>
38
+ </div>
39
+
40
+ <div class="mac__metrics">
41
+ <div><span>{t.chronologicalAgeLabel}</span><strong id="mac-chronological">42</strong></div>
42
+ <div><span>{t.bmrLabel}</span><strong id="mac-bmr">1,620 {t.kcalDay}</strong></div>
43
+ <div><span>{t.tdeeLabel}</span><strong id="mac-tdee">2,511 {t.kcalDay}</strong></div>
44
+ <div><span>{t.idealBmrLabel}</span><strong id="mac-ideal-bmr">1,645 {t.kcalDay}</strong></div>
45
+ </div>
46
+ </section>
47
+
48
+ <section class="mac__controls">
49
+ <div class="mac__topline">
50
+ <div class="mac__seg" role="group" aria-label={t.unitSystemLabel}>
51
+ <button class="mac__seg-btn mac__seg-btn--active" type="button" data-unit="metric">{t.unitMetric}</button>
52
+ <button class="mac__seg-btn" type="button" data-unit="imperial">{t.unitImperial}</button>
53
+ </div>
54
+ <div class="mac__icons" role="group" aria-label={t.sexLabel}>
55
+ <button class="mac__icon-btn" type="button" data-sex="female" title={t.sexFemale}><Icon name="mdi:gender-female" /></button>
56
+ <button class="mac__icon-btn mac__icon-btn--active" type="button" data-sex="male" title={t.sexMale}><Icon name="mdi:gender-male" /></button>
57
+ </div>
58
+ </div>
59
+
60
+ <label class="mac__field">
61
+ <span><b>{t.ageLabel}</b><output id="mac-age-output">42</output></span>
62
+ <input id="mac-age" type="range" min="18" max="85" value="42" />
63
+ </label>
64
+
65
+ <label class="mac__field">
66
+ <span><b>{t.heightLabel}</b><output id="mac-height-output">175 {t.cm}</output></span>
67
+ <input id="mac-height" type="range" min="130" max="220" value="175" />
68
+ </label>
69
+
70
+ <label class="mac__field">
71
+ <span><b>{t.weightLabel}</b><output id="mac-weight-output">75 {t.kg}</output></span>
72
+ <input id="mac-weight" type="range" min="35" max="180" value="75" />
73
+ </label>
74
+
75
+ <label class="mac__field mac__field--fat">
76
+ <span><b>{t.bodyFatLabel}</b><output id="mac-fat-output">22{t.percent}</output></span>
77
+ <input id="mac-fat" type="range" min="5" max="55" value="22" />
78
+ <small>{t.bodyFatHint}</small>
79
+ </label>
80
+
81
+ <div class="mac__activity" role="group" aria-label={t.activityLabel}>
82
+ <button type="button" data-activity="sedentary">{t.activitySedentary}</button>
83
+ <button type="button" data-activity="light">{t.activityLight}</button>
84
+ <button class="mac__activity-active" type="button" data-activity="moderate">{t.activityModerate}</button>
85
+ <button type="button" data-activity="veryActive">{t.activityVeryActive}</button>
86
+ </div>
87
+ </section>
88
+
89
+ <section class="mac__advice">
90
+ <div class="mac__advice-head">
91
+ <Icon name="mdi:lightbulb-on-outline" />
92
+ <strong>{t.adviceTitle}</strong>
93
+ </div>
94
+ <div id="mac-advice-main" class="mac__advice-card">{t.adviceBalanced}</div>
95
+ <div class="mac__advice-row">
96
+ <div><Icon name="mdi:dumbbell" /><span>{t.adviceTraining}</span></div>
97
+ <div><Icon name="mdi:food-drumstick-outline" /><span>{t.adviceProtein}</span></div>
98
+ </div>
99
+ <p><strong>{t.disclaimerTitle}</strong> {t.disclaimerText}</p>
100
+ </section>
101
+ </div>
102
+ </div>
103
+
104
+ <script>
105
+ type Unit = 'metric' | 'imperial';
106
+ type Sex = 'female' | 'male';
107
+ type Activity = 'sedentary' | 'light' | 'moderate' | 'veryActive';
108
+
109
+ function requireEl<T extends Element>(root: Element, selector: string): T {
110
+ const el = root.querySelector<T>(selector);
111
+ if (!el) throw new Error(`Missing ${selector}`);
112
+ return el;
113
+ }
114
+
115
+ function kgToLb(kg: number) { return kg * 2.2046226218; }
116
+ function lbToKg(lb: number) { return lb / 2.2046226218; }
117
+ function cmToIn(cm: number) { return cm / 2.54; }
118
+ function inToCm(inches: number) { return inches * 2.54; }
119
+ function mifflin(weightKg: number, heightCm: number, age: number, sex: Sex) { return 10 * weightKg + 6.25 * heightCm - 5 * age + (sex === 'male' ? 5 : -161); }
120
+ function katch(weightKg: number, fat: number) { return 370 + 21.6 * weightKg * (1 - fat / 100); }
121
+ function idealFat(age: number, sex: Sex) {
122
+ if (sex === 'male') {
123
+ if (age < 30) return 13;
124
+ if (age < 50) return 16;
125
+ return 19;
126
+ }
127
+
128
+ if (age < 30) return 21;
129
+ if (age < 50) return 23;
130
+ return 25;
131
+ }
132
+
133
+ function getCategoryLabel(cat: string, ui: Record<string, string>) {
134
+ if (cat === 'young') return ui.categoryYoung;
135
+ if (cat === 'balanced') return ui.categoryBalanced;
136
+ return ui.categoryAged;
137
+ }
138
+
139
+ function getAdvice(cat: string, ui: Record<string, string>) {
140
+ if (cat === 'young') return ui.adviceYoung;
141
+ if (cat === 'balanced') return ui.adviceBalanced;
142
+ return ui.adviceAged;
143
+ }
144
+
145
+ function getDeltaLabel(ageDelta: number, ui: Record<string, string>) {
146
+ if (ageDelta < 0) return `${Math.abs(ageDelta)} ${ui.deltaYounger}`;
147
+ if (ageDelta > 0) return `${ageDelta} ${ui.deltaOlder}`;
148
+ return ui.deltaAligned;
149
+ }
150
+
151
+ // eslint-disable-next-line max-lines-per-function
152
+ function initMAC(root: Element) {
153
+ const ui = JSON.parse((root as HTMLElement).dataset.ui || '{}') as Record<string, string>;
154
+ const state = { unit: 'metric' as Unit, sex: 'male' as Sex, activity: 'moderate' as Activity, age: 42, heightCm: 175, weightKg: 75, fat: 22 };
155
+ const activityMultipliers: Record<Activity, number> = { sedentary: 1.2, light: 1.375, moderate: 1.55, veryActive: 1.725 };
156
+ const age = requireEl<HTMLInputElement>(root, '#mac-age');
157
+ const height = requireEl<HTMLInputElement>(root, '#mac-height');
158
+ const weight = requireEl<HTMLInputElement>(root, '#mac-weight');
159
+ const fat = requireEl<HTMLInputElement>(root, '#mac-fat');
160
+ const ageOutput = requireEl<HTMLOutputElement>(root, '#mac-age-output');
161
+ const heightOutput = requireEl<HTMLOutputElement>(root, '#mac-height-output');
162
+ const weightOutput = requireEl<HTMLOutputElement>(root, '#mac-weight-output');
163
+ const fatOutput = requireEl<HTMLOutputElement>(root, '#mac-fat-output');
164
+ const dial = requireEl<HTMLElement>(root, '.mac__dial');
165
+ const metabolicAge = requireEl<HTMLElement>(root, '#mac-metabolic-age');
166
+ const delta = requireEl<HTMLElement>(root, '#mac-delta');
167
+ const category = requireEl<HTMLElement>(root, '#mac-category');
168
+ const scoreBar = requireEl<HTMLElement>(root, '#mac-score-bar');
169
+ const chronological = requireEl<HTMLElement>(root, '#mac-chronological');
170
+ const bmrEl = requireEl<HTMLElement>(root, '#mac-bmr');
171
+ const tdeeEl = requireEl<HTMLElement>(root, '#mac-tdee');
172
+ const idealBmrEl = requireEl<HTMLElement>(root, '#mac-ideal-bmr');
173
+ const advice = requireEl<HTMLElement>(root, '#mac-advice-main');
174
+
175
+ function formatMass(valueKg: number) {
176
+ return state.unit === 'imperial' ? `${Math.round(kgToLb(valueKg))} ${ui.lb}` : `${Math.round(valueKg)} ${ui.kg}`;
177
+ }
178
+
179
+ function syncUnitControls() {
180
+ if (state.unit === 'imperial') {
181
+ height.min = '51'; height.max = '87'; height.step = '0.5'; height.value = cmToIn(state.heightCm).toFixed(1);
182
+ weight.min = '77'; weight.max = '397'; weight.step = '1'; weight.value = String(Math.round(kgToLb(state.weightKg)));
183
+ } else {
184
+ height.min = '130'; height.max = '220'; height.step = '1'; height.value = String(Math.round(state.heightCm));
185
+ weight.min = '35'; weight.max = '180'; weight.step = '1'; weight.value = String(Math.round(state.weightKg));
186
+ }
187
+ }
188
+
189
+ function render() {
190
+ const bmr = katch(state.weightKg, state.fat);
191
+ const idealBmr = katch(state.weightKg, idealFat(state.age, state.sex));
192
+ const metabolic = Math.max(12, Math.min(95, Math.round(state.age + (state.fat - idealFat(state.age, state.sex)) * 0.8 + ((idealBmr - bmr) / 20))));
193
+ const ageDelta = metabolic - state.age;
194
+ const score = Math.max(0, Math.min(100, 62 - ageDelta * 3 + (activityMultipliers[state.activity] - 1.2) * 22));
195
+ let cat = 'balanced';
196
+ if (ageDelta <= -5) cat = 'young';
197
+ if (ageDelta > 5) cat = 'aged';
198
+ ageOutput.textContent = String(state.age);
199
+ heightOutput.textContent = state.unit === 'imperial' ? `${cmToIn(state.heightCm).toFixed(1)} ${ui.in}` : `${Math.round(state.heightCm)} ${ui.cm}`;
200
+ weightOutput.textContent = formatMass(state.weightKg);
201
+ fatOutput.textContent = `${state.fat}${ui.percent}`;
202
+ metabolicAge.textContent = String(metabolic);
203
+ chronological.textContent = String(state.age);
204
+ bmrEl.textContent = `${Math.round(bmr).toLocaleString()} ${ui.kcalDay}`;
205
+ tdeeEl.textContent = `${Math.round(mifflin(state.weightKg, state.heightCm, state.age, state.sex) * activityMultipliers[state.activity]).toLocaleString()} ${ui.kcalDay}`;
206
+ idealBmrEl.textContent = `${Math.round(idealBmr).toLocaleString()} ${ui.kcalDay}`;
207
+ category.textContent = getCategoryLabel(cat, ui);
208
+ advice.textContent = getAdvice(cat, ui);
209
+ delta.textContent = getDeltaLabel(ageDelta, ui);
210
+ scoreBar.style.width = `${score}%`;
211
+ dial.style.setProperty('--needle', `${Math.max(-80, Math.min(80, (metabolic - 45) * 3))}deg`);
212
+ dial.style.setProperty('--chrono', `${Math.max(-80, Math.min(80, (state.age - 45) * 3))}deg`);
213
+ root.classList.toggle('mac--young', cat === 'young');
214
+ root.classList.toggle('mac--aged', cat === 'aged');
215
+ }
216
+
217
+ age.addEventListener('input', () => { state.age = Number(age.value); render(); });
218
+ fat.addEventListener('input', () => { state.fat = Number(fat.value); render(); });
219
+ height.addEventListener('input', () => { state.heightCm = state.unit === 'imperial' ? inToCm(Number(height.value)) : Number(height.value); render(); });
220
+ weight.addEventListener('input', () => { state.weightKg = state.unit === 'imperial' ? lbToKg(Number(weight.value)) : Number(weight.value); render(); });
221
+ root.querySelectorAll<HTMLButtonElement>('[data-unit]').forEach((button) => button.addEventListener('click', () => {
222
+ state.unit = button.dataset.unit as Unit;
223
+ root.querySelectorAll<HTMLButtonElement>('[data-unit]').forEach((item) => item.classList.toggle('mac__seg-btn--active', item === button));
224
+ syncUnitControls();
225
+ render();
226
+ }));
227
+ root.querySelectorAll<HTMLButtonElement>('[data-sex]').forEach((button) => button.addEventListener('click', () => {
228
+ state.sex = button.dataset.sex as Sex;
229
+ root.querySelectorAll<HTMLButtonElement>('[data-sex]').forEach((item) => item.classList.toggle('mac__icon-btn--active', item === button));
230
+ render();
231
+ }));
232
+ root.querySelectorAll<HTMLButtonElement>('[data-activity]').forEach((button) => button.addEventListener('click', () => {
233
+ state.activity = button.dataset.activity as Activity;
234
+ root.querySelectorAll<HTMLButtonElement>('[data-activity]').forEach((item) => item.classList.toggle('mac__activity-active', item === button));
235
+ render();
236
+ }));
237
+ syncUnitControls();
238
+ render();
239
+ }
240
+
241
+ document.querySelectorAll('.mac').forEach(initMAC);
242
+ </script>
@@ -0,0 +1,27 @@
1
+ import type { HealthToolEntry } from '../../types';
2
+ import type { MetabolicAgeCalculatorUI } from './ui';
3
+
4
+ export const metabolicAgeCalculator: HealthToolEntry<MetabolicAgeCalculatorUI> = {
5
+ id: 'metabolicAgeCalculator',
6
+ icons: {
7
+ bg: 'mdi:speedometer',
8
+ fg: 'mdi:human-cane',
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
+ };