@jjlmoya/utils-health 1.17.0 → 1.18.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/data.ts +1 -1
- package/src/entries.ts +3 -1
- package/src/tests/locale_completeness.test.ts +2 -3
- package/src/tests/tool_validation.test.ts +2 -2
- package/src/tool/leanBodyMassCalculator/bibliography.astro +14 -0
- package/src/tool/leanBodyMassCalculator/bibliography.ts +20 -0
- package/src/tool/leanBodyMassCalculator/component.astro +240 -0
- package/src/tool/leanBodyMassCalculator/entry.ts +27 -0
- package/src/tool/leanBodyMassCalculator/i18n/de.ts +333 -0
- package/src/tool/leanBodyMassCalculator/i18n/en.ts +333 -0
- package/src/tool/leanBodyMassCalculator/i18n/es.ts +333 -0
- package/src/tool/leanBodyMassCalculator/i18n/fr.ts +333 -0
- package/src/tool/leanBodyMassCalculator/i18n/id.ts +333 -0
- package/src/tool/leanBodyMassCalculator/i18n/it.ts +333 -0
- package/src/tool/leanBodyMassCalculator/i18n/ja.ts +333 -0
- package/src/tool/leanBodyMassCalculator/i18n/ko.ts +333 -0
- package/src/tool/leanBodyMassCalculator/i18n/nl.ts +333 -0
- package/src/tool/leanBodyMassCalculator/i18n/pl.ts +333 -0
- package/src/tool/leanBodyMassCalculator/i18n/pt.ts +333 -0
- package/src/tool/leanBodyMassCalculator/i18n/ru.ts +333 -0
- package/src/tool/leanBodyMassCalculator/i18n/sv.ts +333 -0
- package/src/tool/leanBodyMassCalculator/i18n/tr.ts +333 -0
- package/src/tool/leanBodyMassCalculator/i18n/zh.ts +333 -0
- package/src/tool/leanBodyMassCalculator/index.ts +12 -0
- package/src/tool/leanBodyMassCalculator/lean-body-mass-calculator.css +391 -0
- package/src/tool/leanBodyMassCalculator/logic.ts +55 -0
- package/src/tool/leanBodyMassCalculator/seo.astro +15 -0
- package/src/tool/leanBodyMassCalculator/ui.ts +30 -0
- package/src/tools.ts +2 -1
package/package.json
CHANGED
package/src/category/index.ts
CHANGED
|
@@ -11,10 +11,11 @@ import { binauralTuner } from '../tool/binauralTuner/entry';
|
|
|
11
11
|
import { pelliRobsonTest } from '../tool/pelliRobsonTest/entry';
|
|
12
12
|
import { peripheralVisionTrainer } from '../tool/peripheralVisionTrainer/entry';
|
|
13
13
|
import { epworthSleepinessScale } from '../tool/epworthSleepinessScale/entry';
|
|
14
|
+
import { leanBodyMassCalculator } from '../tool/leanBodyMassCalculator/entry';
|
|
14
15
|
|
|
15
16
|
export const healthCategory: HealthCategoryEntry = {
|
|
16
17
|
icon: 'mdi:heart-pulse',
|
|
17
|
-
tools: [bmiCalculator, bodyAdiposityIndexCalculator, daltonismSimulator, tinnitusReliever, breathingVisualizer, caffeineTracker, waterPurifier, digestionStopwatch, binauralTuner, pelliRobsonTest, peripheralVisionTrainer, epworthSleepinessScale],
|
|
18
|
+
tools: [bmiCalculator, bodyAdiposityIndexCalculator, leanBodyMassCalculator, daltonismSimulator, tinnitusReliever, breathingVisualizer, caffeineTracker, waterPurifier, digestionStopwatch, binauralTuner, pelliRobsonTest, peripheralVisionTrainer, epworthSleepinessScale],
|
|
18
19
|
i18n: {
|
|
19
20
|
en: () => import('./i18n/en').then((m) => m.content),
|
|
20
21
|
es: () => import('./i18n/es').then((m) => m.content),
|
package/src/data.ts
CHANGED
|
@@ -14,6 +14,7 @@ export { screenDecompressionTime } from './tool/screenDecompressionTime';
|
|
|
14
14
|
export { bloodUnitConverter } from './tool/bloodUnitConverter';
|
|
15
15
|
export { readingDistanceCalculator } from './tool/readingDistanceCalculator';
|
|
16
16
|
export { hydrationCalculator } from './tool/hydrationCalculator';
|
|
17
|
+
export { leanBodyMassCalculator } from './tool/leanBodyMassCalculator';
|
|
17
18
|
|
|
18
19
|
export { healthCategory } from './category';
|
|
19
20
|
|
|
@@ -25,4 +26,3 @@ export type {
|
|
|
25
26
|
HealthToolEntry,
|
|
26
27
|
HealthCategoryEntry,
|
|
27
28
|
} from './types';
|
|
28
|
-
|
package/src/entries.ts
CHANGED
|
@@ -8,6 +8,7 @@ export { daltonismSimulator } from './tool/daltonismSimulator/entry';
|
|
|
8
8
|
export { digestionStopwatch } from './tool/digestionStopwatch/entry';
|
|
9
9
|
export { epworthSleepinessScale } from './tool/epworthSleepinessScale/entry';
|
|
10
10
|
export { hydrationCalculator } from './tool/hydrationCalculator/entry';
|
|
11
|
+
export { leanBodyMassCalculator } from './tool/leanBodyMassCalculator/entry';
|
|
11
12
|
export { pelliRobsonTest } from './tool/pelliRobsonTest/entry';
|
|
12
13
|
export { peripheralVisionTrainer } from './tool/peripheralVisionTrainer/entry';
|
|
13
14
|
export { readingDistanceCalculator } from './tool/readingDistanceCalculator/entry';
|
|
@@ -26,6 +27,7 @@ import { daltonismSimulator } from './tool/daltonismSimulator/entry';
|
|
|
26
27
|
import { digestionStopwatch } from './tool/digestionStopwatch/entry';
|
|
27
28
|
import { epworthSleepinessScale } from './tool/epworthSleepinessScale/entry';
|
|
28
29
|
import { hydrationCalculator } from './tool/hydrationCalculator/entry';
|
|
30
|
+
import { leanBodyMassCalculator } from './tool/leanBodyMassCalculator/entry';
|
|
29
31
|
import { pelliRobsonTest } from './tool/pelliRobsonTest/entry';
|
|
30
32
|
import { peripheralVisionTrainer } from './tool/peripheralVisionTrainer/entry';
|
|
31
33
|
import { readingDistanceCalculator } from './tool/readingDistanceCalculator/entry';
|
|
@@ -33,4 +35,4 @@ import { screenDecompressionTime } from './tool/screenDecompressionTime/entry';
|
|
|
33
35
|
import { tinnitusReliever } from './tool/tinnitusReliever/entry';
|
|
34
36
|
import { ubeCalculator } from './tool/ubeCalculator/entry';
|
|
35
37
|
import { waterPurifier } from './tool/waterPurifier/entry';
|
|
36
|
-
export const ALL_ENTRIES = [binauralTuner, bloodUnitConverter, bmiCalculator, bodyAdiposityIndexCalculator, breathingVisualizer, caffeineTracker, daltonismSimulator, digestionStopwatch, epworthSleepinessScale, hydrationCalculator, pelliRobsonTest, peripheralVisionTrainer, readingDistanceCalculator, screenDecompressionTime, tinnitusReliever, ubeCalculator, waterPurifier];
|
|
38
|
+
export const ALL_ENTRIES = [binauralTuner, bloodUnitConverter, bmiCalculator, bodyAdiposityIndexCalculator, breathingVisualizer, caffeineTracker, daltonismSimulator, digestionStopwatch, epworthSleepinessScale, hydrationCalculator, leanBodyMassCalculator, pelliRobsonTest, peripheralVisionTrainer, readingDistanceCalculator, screenDecompressionTime, tinnitusReliever, ubeCalculator, waterPurifier];
|
|
@@ -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 18 tools in ALL_TOOLS', () => {
|
|
8
|
+
expect(ALL_TOOLS.length).toBe(18);
|
|
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 { leanBodyMassCalculator } 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 leanBodyMassCalculator.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: 'Boer P.: Estimated lean body mass as an index for normalization of body fluid volumes in humans',
|
|
6
|
+
url: 'https://doi.org/10.1152/ajprenal.1984.247.4.F632',
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
name: 'Hume R.: Prediction of lean body mass from height and weight',
|
|
10
|
+
url: 'https://doi.org/10.1136/jcp.19.4.389',
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
name: 'NIH MedlinePlus: Body weight and body fat',
|
|
14
|
+
url: 'https://medlineplus.gov/ency/patientinstructions/000348.htm',
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
name: 'Wikipedia: Lean body mass',
|
|
18
|
+
url: 'https://en.wikipedia.org/wiki/Lean_body_mass',
|
|
19
|
+
},
|
|
20
|
+
];
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { Icon } from 'astro-icon/components';
|
|
3
|
+
import type { LeanBodyMassCalculatorUI } from './ui';
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
locale?: string;
|
|
7
|
+
ui?: Record<string, unknown>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const t = (Astro.props.ui || {}) as LeanBodyMassCalculatorUI;
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
<div class="lbm" data-ui={JSON.stringify(t)}>
|
|
14
|
+
<div class="lbm__shell">
|
|
15
|
+
<section class="lbm__stage" aria-live="polite">
|
|
16
|
+
<div class="lbm__figure-wrap">
|
|
17
|
+
<svg class="lbm__figure" viewBox="0 0 180 330" role="img" aria-label={t.silhouetteLean}>
|
|
18
|
+
<defs>
|
|
19
|
+
<clipPath id="lbm-body-clip">
|
|
20
|
+
<path d="M90 18c19 0 34 15 34 34s-15 34-34 34-34-15-34-34 15-34 34-34Zm-45 95c0-16 13-29 29-29h32c16 0 29 13 29 29v70c0 13-6 25-15 33l16 84c2 10-5 19-15 19h-10c-7 0-13-5-15-12l-7-54-7 54c-2 7-8 12-15 12H57c-10 0-17-9-15-19l16-84c-9-8-15-20-15-33v-70Z" />
|
|
21
|
+
</clipPath>
|
|
22
|
+
</defs>
|
|
23
|
+
<path class="lbm__figure-base" d="M90 18c19 0 34 15 34 34s-15 34-34 34-34-15-34-34 15-34 34-34Zm-45 95c0-16 13-29 29-29h32c16 0 29 13 29 29v70c0 13-6 25-15 33l16 84c2 10-5 19-15 19h-10c-7 0-13-5-15-12l-7-54-7 54c-2 7-8 12-15 12H57c-10 0-17-9-15-19l16-84c-9-8-15-20-15-33v-70Z" />
|
|
24
|
+
<g clip-path="url(#lbm-body-clip)">
|
|
25
|
+
<rect class="lbm__fat-fill" x="0" y="0" width="180" height="330" />
|
|
26
|
+
<rect id="lbm-lean-fill" class="lbm__lean-fill" x="0" y="330" width="180" height="0" />
|
|
27
|
+
</g>
|
|
28
|
+
<path class="lbm__figure-outline" d="M90 18c19 0 34 15 34 34s-15 34-34 34-34-15-34-34 15-34 34-34Zm-45 95c0-16 13-29 29-29h32c16 0 29 13 29 29v70c0 13-6 25-15 33l16 84c2 10-5 19-15 19h-10c-7 0-13-5-15-12l-7-54-7 54c-2 7-8 12-15 12H57c-10 0-17-9-15-19l16-84c-9-8-15-20-15-33v-70Z" />
|
|
29
|
+
</svg>
|
|
30
|
+
<div class="lbm__legend">
|
|
31
|
+
<span><i class="lbm__key lbm__key--lean"></i>{t.silhouetteLean}</span>
|
|
32
|
+
<span><i class="lbm__key lbm__key--fat"></i>{t.silhouetteFat}</span>
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
35
|
+
|
|
36
|
+
<div class="lbm__donut" style="
|
|
37
|
+
|
|
38
|
+
--lean: 74">
|
|
39
|
+
<div class="lbm__ring" aria-hidden="true"></div>
|
|
40
|
+
<div class="lbm__ring-core">
|
|
41
|
+
<span>{t.leanShareLabel}</span>
|
|
42
|
+
<strong id="lbm-percent">74.0%</strong>
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
<div class="lbm__metrics">
|
|
47
|
+
<div>
|
|
48
|
+
<span>{t.leanMassLabel}</span>
|
|
49
|
+
<strong id="lbm-lean">55.5 kg</strong>
|
|
50
|
+
</div>
|
|
51
|
+
<div>
|
|
52
|
+
<span>{t.fatMassLabel}</span>
|
|
53
|
+
<strong id="lbm-fat">19.5 kg</strong>
|
|
54
|
+
</div>
|
|
55
|
+
<div>
|
|
56
|
+
<span>{t.totalWeightLabel}</span>
|
|
57
|
+
<strong id="lbm-total">75.0 kg</strong>
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
</section>
|
|
61
|
+
|
|
62
|
+
<section class="lbm__controls">
|
|
63
|
+
<div class="lbm__switches">
|
|
64
|
+
<div class="lbm__seg" role="group" aria-label={t.unitSystemLabel}>
|
|
65
|
+
<button class="lbm__seg-btn lbm__seg-btn--active" type="button" data-unit="metric">{t.unitMetric}</button>
|
|
66
|
+
<button class="lbm__seg-btn" type="button" data-unit="imperial">{t.unitImperial}</button>
|
|
67
|
+
</div>
|
|
68
|
+
<div class="lbm__seg" role="group" aria-label={t.sexLabel}>
|
|
69
|
+
<button class="lbm__icon-btn" type="button" data-sex="female" title={t.sexFemale}><Icon name="mdi:gender-female" /></button>
|
|
70
|
+
<button class="lbm__icon-btn lbm__icon-btn--active" type="button" data-sex="male" title={t.sexMale}><Icon name="mdi:gender-male" /></button>
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
|
|
74
|
+
<label class="lbm__field">
|
|
75
|
+
<span><b>{t.heightLabel}</b><output id="lbm-height-output">175 cm</output></span>
|
|
76
|
+
<input id="lbm-height-range" class="lbm__range" type="range" min="130" max="220" value="175" />
|
|
77
|
+
<input id="lbm-height-number" class="lbm__number" type="number" min="130" max="220" value="175" />
|
|
78
|
+
</label>
|
|
79
|
+
|
|
80
|
+
<label class="lbm__field">
|
|
81
|
+
<span><b>{t.weightLabel}</b><output id="lbm-weight-output">75 kg</output></span>
|
|
82
|
+
<input id="lbm-weight-range" class="lbm__range" type="range" min="35" max="180" value="75" />
|
|
83
|
+
<input id="lbm-weight-number" class="lbm__number" type="number" min="35" max="180" value="75" />
|
|
84
|
+
</label>
|
|
85
|
+
|
|
86
|
+
<div class="lbm__formula" role="group" aria-label={t.formulaLabel}>
|
|
87
|
+
<button class="lbm__formula-btn lbm__formula-btn--active" type="button" data-formula="boer">{t.formulaBoer}</button>
|
|
88
|
+
<button class="lbm__formula-btn" type="button" data-formula="james">{t.formulaJames}</button>
|
|
89
|
+
<button class="lbm__formula-btn" type="button" data-formula="hume">{t.formulaHume}</button>
|
|
90
|
+
</div>
|
|
91
|
+
|
|
92
|
+
<div class="lbm__equation">
|
|
93
|
+
<span><Icon name="mdi:function-variant" /> {t.equationLabel}</span>
|
|
94
|
+
<code id="lbm-equation">0.407W + 0.267H - 19.2</code>
|
|
95
|
+
</div>
|
|
96
|
+
</section>
|
|
97
|
+
|
|
98
|
+
<section class="lbm__notes">
|
|
99
|
+
<div><strong>{t.clinicalNoteTitle}</strong><p>{t.clinicalNoteText}</p></div>
|
|
100
|
+
<div><strong>{t.formulaNoteTitle}</strong><p>{t.formulaNoteText}</p></div>
|
|
101
|
+
</section>
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
|
|
105
|
+
<script>
|
|
106
|
+
type Unit = 'metric' | 'imperial';
|
|
107
|
+
type Sex = 'female' | 'male';
|
|
108
|
+
type Formula = 'boer' | 'james' | 'hume';
|
|
109
|
+
|
|
110
|
+
function requireEl<T extends Element>(root: Element, selector: string): T {
|
|
111
|
+
const el = root.querySelector<T>(selector);
|
|
112
|
+
if (!el) throw new Error(`Missing ${selector}`);
|
|
113
|
+
return el;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function kgToLb(kg: number) { return kg * 2.2046226218; }
|
|
117
|
+
function lbToKg(lb: number) { return lb / 2.2046226218; }
|
|
118
|
+
function cmToIn(cm: number) { return cm / 2.54; }
|
|
119
|
+
function inToCm(inches: number) { return inches * 2.54; }
|
|
120
|
+
|
|
121
|
+
function calculate(weightKg: number, heightCm: number, sex: Sex, formula: Formula): number {
|
|
122
|
+
if (formula === 'james') return sex === 'male' ? 1.1 * weightKg - 128 * (weightKg / heightCm) ** 2 : 1.07 * weightKg - 148 * (weightKg / heightCm) ** 2;
|
|
123
|
+
if (formula === 'hume') return sex === 'male' ? 0.3281 * weightKg + 0.33929 * heightCm - 29.5336 : 0.29569 * weightKg + 0.41813 * heightCm - 43.2933;
|
|
124
|
+
return sex === 'male' ? 0.407 * weightKg + 0.267 * heightCm - 19.2 : 0.252 * weightKg + 0.473 * heightCm - 48.3;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// eslint-disable-next-line max-lines-per-function
|
|
128
|
+
function initLBM(root: Element) {
|
|
129
|
+
const ui = JSON.parse((root as HTMLElement).dataset.ui || '{}') as Record<string, string>;
|
|
130
|
+
const state = { unit: 'metric' as Unit, sex: 'male' as Sex, formula: 'boer' as Formula, heightCm: 175, weightKg: 75 };
|
|
131
|
+
const heightRange = requireEl<HTMLInputElement>(root, '#lbm-height-range');
|
|
132
|
+
const heightNumber = requireEl<HTMLInputElement>(root, '#lbm-height-number');
|
|
133
|
+
const weightRange = requireEl<HTMLInputElement>(root, '#lbm-weight-range');
|
|
134
|
+
const weightNumber = requireEl<HTMLInputElement>(root, '#lbm-weight-number');
|
|
135
|
+
const heightOutput = requireEl<HTMLOutputElement>(root, '#lbm-height-output');
|
|
136
|
+
const weightOutput = requireEl<HTMLOutputElement>(root, '#lbm-weight-output');
|
|
137
|
+
const lean = requireEl<HTMLElement>(root, '#lbm-lean');
|
|
138
|
+
const fat = requireEl<HTMLElement>(root, '#lbm-fat');
|
|
139
|
+
const total = requireEl<HTMLElement>(root, '#lbm-total');
|
|
140
|
+
const percent = requireEl<HTMLElement>(root, '#lbm-percent');
|
|
141
|
+
const donut = requireEl<HTMLElement>(root, '.lbm__donut');
|
|
142
|
+
const fill = requireEl<SVGRectElement>(root, '#lbm-lean-fill');
|
|
143
|
+
const equation = requireEl<HTMLElement>(root, '#lbm-equation');
|
|
144
|
+
|
|
145
|
+
const equations: Record<Formula, Record<Sex, string>> = {
|
|
146
|
+
boer: { male: '0.407W + 0.267H - 19.2', female: '0.252W + 0.473H - 48.3' },
|
|
147
|
+
james: { male: '1.1W - 128(W / H)^2', female: '1.07W - 148(W / H)^2' },
|
|
148
|
+
hume: { male: '0.32810W + 0.33929H - 29.5336', female: '0.29569W + 0.41813H - 43.2933' },
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
function mass(valueKg: number): string {
|
|
152
|
+
return state.unit === 'imperial' ? `${kgToLb(valueKg).toFixed(1)} ${ui.lb}` : `${valueKg.toFixed(1)} ${ui.kg}`;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function syncControls() {
|
|
156
|
+
if (state.unit === 'imperial') {
|
|
157
|
+
heightRange.min = heightNumber.min = '51';
|
|
158
|
+
heightRange.max = heightNumber.max = '87';
|
|
159
|
+
heightRange.step = heightNumber.step = '0.5';
|
|
160
|
+
weightRange.min = weightNumber.min = '77';
|
|
161
|
+
weightRange.max = weightNumber.max = '397';
|
|
162
|
+
weightRange.step = weightNumber.step = '1';
|
|
163
|
+
heightRange.value = heightNumber.value = cmToIn(state.heightCm).toFixed(1);
|
|
164
|
+
weightRange.value = weightNumber.value = Math.round(kgToLb(state.weightKg)).toString();
|
|
165
|
+
} else {
|
|
166
|
+
heightRange.min = heightNumber.min = '130';
|
|
167
|
+
heightRange.max = heightNumber.max = '220';
|
|
168
|
+
heightRange.step = heightNumber.step = '1';
|
|
169
|
+
weightRange.min = weightNumber.min = '35';
|
|
170
|
+
weightRange.max = weightNumber.max = '180';
|
|
171
|
+
weightRange.step = weightNumber.step = '1';
|
|
172
|
+
heightRange.value = heightNumber.value = Math.round(state.heightCm).toString();
|
|
173
|
+
weightRange.value = weightNumber.value = Math.round(state.weightKg).toString();
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function render() {
|
|
178
|
+
const leanKg = Math.max(0, Math.min(state.weightKg, calculate(state.weightKg, state.heightCm, state.sex, state.formula)));
|
|
179
|
+
const fatKg = Math.max(0, state.weightKg - leanKg);
|
|
180
|
+
const share = state.weightKg > 0 ? (leanKg / state.weightKg) * 100 : 0;
|
|
181
|
+
heightOutput.textContent = state.unit === 'imperial' ? `${cmToIn(state.heightCm).toFixed(1)} ${ui.in}` : `${Math.round(state.heightCm)} ${ui.cm}`;
|
|
182
|
+
weightOutput.textContent = mass(state.weightKg);
|
|
183
|
+
lean.textContent = mass(leanKg);
|
|
184
|
+
fat.textContent = mass(fatKg);
|
|
185
|
+
total.textContent = mass(state.weightKg);
|
|
186
|
+
percent.textContent = `${share.toFixed(1)}%`;
|
|
187
|
+
donut.style.setProperty('--lean', share.toFixed(1));
|
|
188
|
+
const leanHeight = Math.max(0, Math.min(330, 330 * (share / 100)));
|
|
189
|
+
fill.setAttribute('y', String(330 - leanHeight));
|
|
190
|
+
fill.setAttribute('height', String(leanHeight));
|
|
191
|
+
equation.textContent = equations[state.formula][state.sex];
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function readHeight(input: HTMLInputElement) {
|
|
195
|
+
state.heightCm = state.unit === 'imperial' ? inToCm(Number(input.value)) : Number(input.value);
|
|
196
|
+
syncControls();
|
|
197
|
+
render();
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function readWeight(input: HTMLInputElement) {
|
|
201
|
+
state.weightKg = state.unit === 'imperial' ? lbToKg(Number(input.value)) : Number(input.value);
|
|
202
|
+
syncControls();
|
|
203
|
+
render();
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
root.querySelectorAll<HTMLButtonElement>('[data-unit]').forEach((button) => {
|
|
207
|
+
button.addEventListener('click', () => {
|
|
208
|
+
state.unit = button.dataset.unit as Unit;
|
|
209
|
+
root.querySelectorAll<HTMLButtonElement>('[data-unit]').forEach((item) => item.classList.toggle('lbm__seg-btn--active', item === button));
|
|
210
|
+
syncControls();
|
|
211
|
+
render();
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
root.querySelectorAll<HTMLButtonElement>('[data-sex]').forEach((button) => {
|
|
216
|
+
button.addEventListener('click', () => {
|
|
217
|
+
state.sex = button.dataset.sex as Sex;
|
|
218
|
+
root.querySelectorAll<HTMLButtonElement>('[data-sex]').forEach((item) => item.classList.toggle('lbm__icon-btn--active', item === button));
|
|
219
|
+
render();
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
root.querySelectorAll<HTMLButtonElement>('[data-formula]').forEach((button) => {
|
|
224
|
+
button.addEventListener('click', () => {
|
|
225
|
+
state.formula = button.dataset.formula as Formula;
|
|
226
|
+
root.querySelectorAll<HTMLButtonElement>('[data-formula]').forEach((item) => item.classList.toggle('lbm__formula-btn--active', item === button));
|
|
227
|
+
render();
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
heightRange.addEventListener('input', () => readHeight(heightRange));
|
|
232
|
+
heightNumber.addEventListener('input', () => readHeight(heightNumber));
|
|
233
|
+
weightRange.addEventListener('input', () => readWeight(weightRange));
|
|
234
|
+
weightNumber.addEventListener('input', () => readWeight(weightNumber));
|
|
235
|
+
syncControls();
|
|
236
|
+
render();
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
document.querySelectorAll('.lbm').forEach(initLBM);
|
|
240
|
+
</script>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { HealthToolEntry } from '../../types';
|
|
2
|
+
import type { LeanBodyMassCalculatorUI } from './ui';
|
|
3
|
+
|
|
4
|
+
export const leanBodyMassCalculator: HealthToolEntry<LeanBodyMassCalculatorUI> = {
|
|
5
|
+
id: 'leanBodyMassCalculator',
|
|
6
|
+
icons: {
|
|
7
|
+
bg: 'mdi:human',
|
|
8
|
+
fg: 'mdi:scale-bathroom',
|
|
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
|
+
};
|