@jjlmoya/utils-health 1.21.0 → 1.22.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 +3 -1
- package/src/entries.ts +5 -1
- package/src/index.ts +2 -0
- package/src/tests/locale_completeness.test.ts +2 -2
- package/src/tests/tool_validation.test.ts +2 -2
- package/src/tool/waistHeightRatioCalculator/bibliography.astro +14 -0
- package/src/tool/waistHeightRatioCalculator/bibliography.ts +16 -0
- package/src/tool/waistHeightRatioCalculator/component.astro +300 -0
- package/src/tool/waistHeightRatioCalculator/entry.ts +27 -0
- package/src/tool/waistHeightRatioCalculator/i18n/de.ts +343 -0
- package/src/tool/waistHeightRatioCalculator/i18n/en.ts +343 -0
- package/src/tool/waistHeightRatioCalculator/i18n/es.ts +343 -0
- package/src/tool/waistHeightRatioCalculator/i18n/fr.ts +343 -0
- package/src/tool/waistHeightRatioCalculator/i18n/id.ts +343 -0
- package/src/tool/waistHeightRatioCalculator/i18n/it.ts +343 -0
- package/src/tool/waistHeightRatioCalculator/i18n/ja.ts +343 -0
- package/src/tool/waistHeightRatioCalculator/i18n/ko.ts +343 -0
- package/src/tool/waistHeightRatioCalculator/i18n/nl.ts +343 -0
- package/src/tool/waistHeightRatioCalculator/i18n/pl.ts +343 -0
- package/src/tool/waistHeightRatioCalculator/i18n/pt.ts +343 -0
- package/src/tool/waistHeightRatioCalculator/i18n/ru.ts +343 -0
- package/src/tool/waistHeightRatioCalculator/i18n/sv.ts +343 -0
- package/src/tool/waistHeightRatioCalculator/i18n/tr.ts +343 -0
- package/src/tool/waistHeightRatioCalculator/i18n/zh.ts +343 -0
- package/src/tool/waistHeightRatioCalculator/index.ts +12 -0
- package/src/tool/waistHeightRatioCalculator/logic.ts +29 -0
- package/src/tool/waistHeightRatioCalculator/seo.astro +15 -0
- package/src/tool/waistHeightRatioCalculator/ui.ts +34 -0
- package/src/tool/waistHeightRatioCalculator/waist-height-ratio-calculator.css +368 -0
- package/src/tool/waistHipRatioCalculator/bibliography.astro +14 -0
- package/src/tool/waistHipRatioCalculator/bibliography.ts +24 -0
- package/src/tool/waistHipRatioCalculator/component.astro +316 -0
- package/src/tool/waistHipRatioCalculator/entry.ts +27 -0
- package/src/tool/waistHipRatioCalculator/i18n/de.ts +316 -0
- package/src/tool/waistHipRatioCalculator/i18n/en.ts +316 -0
- package/src/tool/waistHipRatioCalculator/i18n/es.ts +316 -0
- package/src/tool/waistHipRatioCalculator/i18n/fr.ts +316 -0
- package/src/tool/waistHipRatioCalculator/i18n/id.ts +316 -0
- package/src/tool/waistHipRatioCalculator/i18n/it.ts +316 -0
- package/src/tool/waistHipRatioCalculator/i18n/ja.ts +316 -0
- package/src/tool/waistHipRatioCalculator/i18n/ko.ts +316 -0
- package/src/tool/waistHipRatioCalculator/i18n/nl.ts +316 -0
- package/src/tool/waistHipRatioCalculator/i18n/pl.ts +316 -0
- package/src/tool/waistHipRatioCalculator/i18n/pt.ts +316 -0
- package/src/tool/waistHipRatioCalculator/i18n/ru.ts +316 -0
- package/src/tool/waistHipRatioCalculator/i18n/sv.ts +316 -0
- package/src/tool/waistHipRatioCalculator/i18n/tr.ts +316 -0
- package/src/tool/waistHipRatioCalculator/i18n/zh.ts +316 -0
- package/src/tool/waistHipRatioCalculator/index.ts +12 -0
- package/src/tool/waistHipRatioCalculator/logic.ts +36 -0
- package/src/tool/waistHipRatioCalculator/seo.astro +15 -0
- package/src/tool/waistHipRatioCalculator/ui.ts +35 -0
- package/src/tool/waistHipRatioCalculator/waist-hip-ratio-calculator.css +433 -0
- package/src/tools.ts +3 -1
package/package.json
CHANGED
package/src/category/index.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { HealthCategoryEntry } from '../types';
|
|
2
2
|
import { bmiCalculator } from '../tool/bmiCalculator/entry';
|
|
3
3
|
import { bodyAdiposityIndexCalculator } from '../tool/bodyAdiposityIndexCalculator/entry';
|
|
4
|
+
import { waistHipRatioCalculator } from '../tool/waistHipRatioCalculator/entry';
|
|
5
|
+
import { waistHeightRatioCalculator } from '../tool/waistHeightRatioCalculator/entry';
|
|
4
6
|
import { daltonismSimulator } from '../tool/daltonismSimulator/entry';
|
|
5
7
|
import { tinnitusReliever } from '../tool/tinnitusReliever/entry';
|
|
6
8
|
import { breathingVisualizer } from '../tool/breathingVisualizer/entry';
|
|
@@ -18,7 +20,7 @@ import { metabolicAgeCalculator } from '../tool/metabolicAgeCalculator/entry';
|
|
|
18
20
|
|
|
19
21
|
export const healthCategory: HealthCategoryEntry = {
|
|
20
22
|
icon: 'mdi:heart-pulse',
|
|
21
|
-
tools: [bmiCalculator, bodyAdiposityIndexCalculator, leanBodyMassCalculator, metabolicAgeCalculator, caloricDeficitCalculator, dailyProteinCalculator, daltonismSimulator, tinnitusReliever, breathingVisualizer, caffeineTracker, waterPurifier, digestionStopwatch, binauralTuner, pelliRobsonTest, peripheralVisionTrainer, epworthSleepinessScale],
|
|
23
|
+
tools: [bmiCalculator, bodyAdiposityIndexCalculator, waistHipRatioCalculator, waistHeightRatioCalculator, leanBodyMassCalculator, metabolicAgeCalculator, caloricDeficitCalculator, dailyProteinCalculator, daltonismSimulator, tinnitusReliever, breathingVisualizer, caffeineTracker, waterPurifier, digestionStopwatch, binauralTuner, pelliRobsonTest, peripheralVisionTrainer, epworthSleepinessScale],
|
|
22
24
|
i18n: {
|
|
23
25
|
en: () => import('./i18n/en').then((m) => m.content),
|
|
24
26
|
es: () => import('./i18n/es').then((m) => m.content),
|
package/src/entries.ts
CHANGED
|
@@ -2,6 +2,8 @@ export { binauralTuner } from './tool/binauralTuner/entry';
|
|
|
2
2
|
export { bloodUnitConverter } from './tool/bloodUnitConverter/entry';
|
|
3
3
|
export { bmiCalculator } from './tool/bmiCalculator/entry';
|
|
4
4
|
export { bodyAdiposityIndexCalculator } from './tool/bodyAdiposityIndexCalculator/entry';
|
|
5
|
+
export { waistHipRatioCalculator } from './tool/waistHipRatioCalculator/entry';
|
|
6
|
+
export { waistHeightRatioCalculator } from './tool/waistHeightRatioCalculator/entry';
|
|
5
7
|
export { breathingVisualizer } from './tool/breathingVisualizer/entry';
|
|
6
8
|
export { caffeineTracker } from './tool/caffeineTracker/entry';
|
|
7
9
|
export { caloricDeficitCalculator } from './tool/caloricDeficitCalculator/entry';
|
|
@@ -24,6 +26,8 @@ import { binauralTuner } from './tool/binauralTuner/entry';
|
|
|
24
26
|
import { bloodUnitConverter } from './tool/bloodUnitConverter/entry';
|
|
25
27
|
import { bmiCalculator } from './tool/bmiCalculator/entry';
|
|
26
28
|
import { bodyAdiposityIndexCalculator } from './tool/bodyAdiposityIndexCalculator/entry';
|
|
29
|
+
import { waistHipRatioCalculator } from './tool/waistHipRatioCalculator/entry';
|
|
30
|
+
import { waistHeightRatioCalculator } from './tool/waistHeightRatioCalculator/entry';
|
|
27
31
|
import { breathingVisualizer } from './tool/breathingVisualizer/entry';
|
|
28
32
|
import { caffeineTracker } from './tool/caffeineTracker/entry';
|
|
29
33
|
import { caloricDeficitCalculator } from './tool/caloricDeficitCalculator/entry';
|
|
@@ -41,4 +45,4 @@ import { screenDecompressionTime } from './tool/screenDecompressionTime/entry';
|
|
|
41
45
|
import { tinnitusReliever } from './tool/tinnitusReliever/entry';
|
|
42
46
|
import { ubeCalculator } from './tool/ubeCalculator/entry';
|
|
43
47
|
import { waterPurifier } from './tool/waterPurifier/entry';
|
|
44
|
-
export const ALL_ENTRIES = [binauralTuner, bloodUnitConverter, bmiCalculator, bodyAdiposityIndexCalculator, breathingVisualizer, caffeineTracker, caloricDeficitCalculator, dailyProteinCalculator, daltonismSimulator, digestionStopwatch, epworthSleepinessScale, hydrationCalculator, leanBodyMassCalculator, metabolicAgeCalculator, pelliRobsonTest, peripheralVisionTrainer, readingDistanceCalculator, screenDecompressionTime, tinnitusReliever, ubeCalculator, waterPurifier];
|
|
48
|
+
export const ALL_ENTRIES = [binauralTuner, bloodUnitConverter, bmiCalculator, bodyAdiposityIndexCalculator, waistHipRatioCalculator, waistHeightRatioCalculator, breathingVisualizer, caffeineTracker, caloricDeficitCalculator, dailyProteinCalculator, daltonismSimulator, digestionStopwatch, epworthSleepinessScale, hydrationCalculator, leanBodyMassCalculator, metabolicAgeCalculator, pelliRobsonTest, peripheralVisionTrainer, readingDistanceCalculator, screenDecompressionTime, tinnitusReliever, ubeCalculator, waterPurifier];
|
package/src/index.ts
CHANGED
|
@@ -16,6 +16,8 @@ export * from './tool/bloodUnitConverter';
|
|
|
16
16
|
export * from './tool/readingDistanceCalculator';
|
|
17
17
|
export * from './tool/hydrationCalculator';
|
|
18
18
|
export * from './tool/bodyAdiposityIndexCalculator';
|
|
19
|
+
export * from './tool/waistHipRatioCalculator';
|
|
20
|
+
export * from './tool/waistHeightRatioCalculator';
|
|
19
21
|
export * from './tool/dailyProteinCalculator';
|
|
20
22
|
export * from './tool/leanBodyMassCalculator';
|
|
21
23
|
export * from './tool/metabolicAgeCalculator';
|
|
@@ -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 23 tools in ALL_TOOLS', () => {
|
|
8
|
+
expect(ALL_TOOLS.length).toBe(23);
|
|
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 { waistHeightRatioCalculator } 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 waistHeightRatioCalculator.i18n[locale]?.();
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
{content && <Bibliography links={content.bibliography} />}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { BibliographyEntry } from '../../types';
|
|
2
|
+
|
|
3
|
+
export const bibliography: BibliographyEntry[] = [
|
|
4
|
+
{
|
|
5
|
+
name: 'WHO: Waist circumference and waist-hip ratio expert consultation',
|
|
6
|
+
url: 'https://apps.who.int/iris/handle/10665/44583',
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
name: 'NICE guideline: Obesity identification and classification',
|
|
10
|
+
url: 'https://www.nice.org.uk/guidance/ng246/chapter/Recommendations',
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
name: 'Wikipedia: Waist-to-height ratio',
|
|
14
|
+
url: 'https://en.wikipedia.org/wiki/Waist-to-height_ratio',
|
|
15
|
+
},
|
|
16
|
+
];
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { Icon } from 'astro-icon/components';
|
|
3
|
+
import type { WaistHeightRatioCalculatorUI } from './ui';
|
|
4
|
+
import './waist-height-ratio-calculator.css';
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
locale?: string;
|
|
8
|
+
ui?: Record<string, unknown>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const t = (Astro.props.ui || {}) as WaistHeightRatioCalculatorUI;
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
<div class="whtr" data-ui={JSON.stringify(t)}>
|
|
15
|
+
<div class="whtr__panel" data-level="healthy">
|
|
16
|
+
<div class="whtr__measure" aria-label={t.tapeAriaLabel}>
|
|
17
|
+
<div class="whtr__tape">
|
|
18
|
+
<div class="whtr__ticks" aria-hidden="true">
|
|
19
|
+
{Array.from({ length: 13 }).map((_, index) => <span style={`--i:${index}`}></span>)}
|
|
20
|
+
</div>
|
|
21
|
+
<svg class="whtr__person" viewBox="0 0 180 300" role="img" aria-hidden="true">
|
|
22
|
+
<defs>
|
|
23
|
+
<linearGradient id="whtrBodyGradient" x1="0" x2="1" y1="0" y2="1">
|
|
24
|
+
<stop offset="0" stop-color="currentColor" stop-opacity="0.16" />
|
|
25
|
+
<stop offset="0.56" stop-color="currentColor" stop-opacity="0.34" />
|
|
26
|
+
<stop offset="1" stop-color="currentColor" stop-opacity="0.2" />
|
|
27
|
+
</linearGradient>
|
|
28
|
+
</defs>
|
|
29
|
+
<circle class="whtr__body-shape" cx="90" cy="31" r="24" />
|
|
30
|
+
<path
|
|
31
|
+
class="whtr__body-shape whtr__torso"
|
|
32
|
+
d="M58 66 C69 53 111 53 122 66 C135 82 137 111 124 133 C116 147 118 166 134 188 C147 207 150 242 139 276 L116 276 C121 240 113 213 96 195 C92 191 88 191 84 195 C67 213 59 240 64 276 L41 276 C30 242 33 207 46 188 C62 166 64 147 56 133 C43 111 45 82 58 66 Z"
|
|
33
|
+
/>
|
|
34
|
+
<path class="whtr__arm" d="M58 74 C35 103 29 151 40 203" />
|
|
35
|
+
<path class="whtr__arm" d="M122 74 C145 103 151 151 140 203" />
|
|
36
|
+
<ellipse id="whtr-waist-band" class="whtr__waist-band" cx="90" cy="137" rx="36" ry="10" />
|
|
37
|
+
<ellipse id="whtr-hip-band" class="whtr__hip-band" cx="90" cy="190" rx="56" ry="13" />
|
|
38
|
+
</svg>
|
|
39
|
+
<div class="whtr__half-line" id="whtr-half-line">
|
|
40
|
+
<strong>{t.halfHeightLabel}</strong>
|
|
41
|
+
<span id="whtr-half-value">85 cm</span>
|
|
42
|
+
</div>
|
|
43
|
+
<div class="whtr__waist-line" id="whtr-waist-line">
|
|
44
|
+
<strong>{t.currentWaistLabel}</strong>
|
|
45
|
+
<span id="whtr-waist-tag">82 cm</span>
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
<div class="whtr__rule">
|
|
49
|
+
<Icon name="mdi:target-variant" />
|
|
50
|
+
<span>{t.safeWaistLabel}</span>
|
|
51
|
+
<strong id="whtr-safe-limit">85 cm</strong>
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
|
|
55
|
+
<div class="whtr__controls">
|
|
56
|
+
<div class="whtr__unit" role="group" aria-label={t.unitSystemLabel}>
|
|
57
|
+
<button class="whtr__unit-btn whtr__unit-btn--active" type="button" data-unit="metric">{t.unitMetric}</button>
|
|
58
|
+
<button class="whtr__unit-btn" type="button" data-unit="imperial">{t.unitImperial}</button>
|
|
59
|
+
</div>
|
|
60
|
+
|
|
61
|
+
<label class="whtr__field">
|
|
62
|
+
<span class="whtr__field-top"><span>{t.labelHeight}</span><strong id="whtr-height-value">{t.initialHeight}</strong></span>
|
|
63
|
+
<input id="whtr-height" class="whtr__range" type="range" min="90" max="220" value="170" />
|
|
64
|
+
</label>
|
|
65
|
+
|
|
66
|
+
<label class="whtr__field">
|
|
67
|
+
<span class="whtr__field-top"><span>{t.labelWaist}</span><strong id="whtr-waist-value">{t.initialWaist}</strong></span>
|
|
68
|
+
<input id="whtr-waist" class="whtr__range" type="range" min="35" max="160" value="82" />
|
|
69
|
+
</label>
|
|
70
|
+
|
|
71
|
+
<div class="whtr__status">
|
|
72
|
+
<div>
|
|
73
|
+
<span>{t.ratioLabel}</span>
|
|
74
|
+
<strong id="whtr-ratio">{t.initialRatio}</strong>
|
|
75
|
+
</div>
|
|
76
|
+
<div>
|
|
77
|
+
<span>{t.riskLabel}</span>
|
|
78
|
+
<strong id="whtr-level">{t.levelHealthy}</strong>
|
|
79
|
+
</div>
|
|
80
|
+
<p id="whtr-desc">{t.descHealthy}</p>
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
|
|
84
|
+
<div class="whtr__notes">
|
|
85
|
+
<div class="whtr__note">
|
|
86
|
+
<Icon name="mdi:function-variant" />
|
|
87
|
+
<p><strong>{t.formulaLabel}</strong> {t.formulaText}</p>
|
|
88
|
+
</div>
|
|
89
|
+
<div class="whtr__note">
|
|
90
|
+
<Icon name="mdi:tape-measure" />
|
|
91
|
+
<p><strong>{t.ruleLabel}</strong> {t.ruleText}</p>
|
|
92
|
+
</div>
|
|
93
|
+
<div class="whtr__note">
|
|
94
|
+
<Icon name="mdi:human-handsup" />
|
|
95
|
+
<p><strong>{t.measurementLabel}</strong> {t.measurementText}</p>
|
|
96
|
+
</div>
|
|
97
|
+
<div class="whtr__note whtr__note--alert">
|
|
98
|
+
<Icon name="mdi:alert-circle-outline" />
|
|
99
|
+
<p><strong>{t.noteLabel}</strong> {t.noteText}</p>
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
|
|
105
|
+
<script>
|
|
106
|
+
type Unit = 'metric' | 'imperial';
|
|
107
|
+
type Level = 'veryThin' | 'healthy' | 'overweight' | 'obese';
|
|
108
|
+
|
|
109
|
+
interface WHtRElements {
|
|
110
|
+
panel: HTMLElement;
|
|
111
|
+
height: HTMLInputElement;
|
|
112
|
+
waist: HTMLInputElement;
|
|
113
|
+
heightValue: HTMLElement;
|
|
114
|
+
waistValue: HTMLElement;
|
|
115
|
+
halfValue: HTMLElement;
|
|
116
|
+
waistTag: HTMLElement;
|
|
117
|
+
safeLimit: HTMLElement;
|
|
118
|
+
ratio: HTMLElement;
|
|
119
|
+
level: HTMLElement;
|
|
120
|
+
desc: HTMLElement;
|
|
121
|
+
halfLine: HTMLElement;
|
|
122
|
+
waistLine: HTMLElement;
|
|
123
|
+
waistBand: SVGEllipseElement;
|
|
124
|
+
hipBand: SVGEllipseElement;
|
|
125
|
+
unitBtns: NodeListOf<HTMLButtonElement>;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
interface WHtRState {
|
|
129
|
+
unit: Unit;
|
|
130
|
+
heightCm: number;
|
|
131
|
+
waistCm: number;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const STORAGE_KEY = 'whtr_state';
|
|
135
|
+
|
|
136
|
+
function required<T extends Element>(container: Element, selector: string): T {
|
|
137
|
+
const element = container.querySelector<T>(selector);
|
|
138
|
+
if (!element) throw new Error(selector);
|
|
139
|
+
return element;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function cmToIn(cm: number): number {
|
|
143
|
+
return cm / 2.54;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function inToCm(inches: number): number {
|
|
147
|
+
return inches * 2.54;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function isUnit(value: unknown): value is Unit {
|
|
151
|
+
return value === 'metric' || value === 'imperial';
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function clamp(value: number, min: number, max: number): number {
|
|
155
|
+
return Math.max(min, Math.min(max, value));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function classify(value: number): Level {
|
|
159
|
+
if (value < 0.4) return 'veryThin';
|
|
160
|
+
if (value < 0.5) return 'healthy';
|
|
161
|
+
if (value < 0.6) return 'overweight';
|
|
162
|
+
return 'obese';
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function getElements(container: Element): WHtRElements | null {
|
|
166
|
+
try {
|
|
167
|
+
return {
|
|
168
|
+
panel: required<HTMLElement>(container, '.whtr__panel'),
|
|
169
|
+
height: required<HTMLInputElement>(container, '#whtr-height'),
|
|
170
|
+
waist: required<HTMLInputElement>(container, '#whtr-waist'),
|
|
171
|
+
heightValue: required<HTMLElement>(container, '#whtr-height-value'),
|
|
172
|
+
waistValue: required<HTMLElement>(container, '#whtr-waist-value'),
|
|
173
|
+
halfValue: required<HTMLElement>(container, '#whtr-half-value'),
|
|
174
|
+
waistTag: required<HTMLElement>(container, '#whtr-waist-tag'),
|
|
175
|
+
safeLimit: required<HTMLElement>(container, '#whtr-safe-limit'),
|
|
176
|
+
ratio: required<HTMLElement>(container, '#whtr-ratio'),
|
|
177
|
+
level: required<HTMLElement>(container, '#whtr-level'),
|
|
178
|
+
desc: required<HTMLElement>(container, '#whtr-desc'),
|
|
179
|
+
halfLine: required<HTMLElement>(container, '#whtr-half-line'),
|
|
180
|
+
waistLine: required<HTMLElement>(container, '#whtr-waist-line'),
|
|
181
|
+
waistBand: required<SVGEllipseElement>(container, '#whtr-waist-band'),
|
|
182
|
+
hipBand: required<SVGEllipseElement>(container, '#whtr-hip-band'),
|
|
183
|
+
unitBtns: container.querySelectorAll<HTMLButtonElement>('.whtr__unit-btn'),
|
|
184
|
+
};
|
|
185
|
+
} catch {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function formatLength(cm: number, unit: Unit, ui: Record<string, string>): string {
|
|
191
|
+
if (unit === 'imperial') return `${cmToIn(cm).toFixed(1)} ${ui.unitIn}`;
|
|
192
|
+
return `${Math.round(cm)} ${ui.unitCm}`;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function loadState(): WHtRState {
|
|
196
|
+
const fallback: WHtRState = { unit: 'metric', heightCm: 170, waistCm: 82 };
|
|
197
|
+
try {
|
|
198
|
+
const raw = window.localStorage.getItem(STORAGE_KEY);
|
|
199
|
+
if (!raw) return fallback;
|
|
200
|
+
const saved = JSON.parse(raw) as Partial<WHtRState>;
|
|
201
|
+
return {
|
|
202
|
+
unit: isUnit(saved.unit) ? saved.unit : fallback.unit,
|
|
203
|
+
heightCm: clamp(Number(saved.heightCm) || fallback.heightCm, 90, 220),
|
|
204
|
+
waistCm: clamp(Number(saved.waistCm) || fallback.waistCm, 35, 160),
|
|
205
|
+
};
|
|
206
|
+
} catch {
|
|
207
|
+
return fallback;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function saveState(state: WHtRState) {
|
|
212
|
+
try {
|
|
213
|
+
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
|
|
214
|
+
} catch {
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function syncRanges(elements: WHtRElements, state: WHtRState) {
|
|
219
|
+
if (state.unit === 'imperial') {
|
|
220
|
+
elements.height.min = '35';
|
|
221
|
+
elements.height.max = '87';
|
|
222
|
+
elements.height.step = '0.5';
|
|
223
|
+
elements.height.value = cmToIn(state.heightCm).toFixed(1);
|
|
224
|
+
elements.waist.min = '14';
|
|
225
|
+
elements.waist.max = '63';
|
|
226
|
+
elements.waist.step = '0.5';
|
|
227
|
+
elements.waist.value = cmToIn(state.waistCm).toFixed(1);
|
|
228
|
+
} else {
|
|
229
|
+
elements.height.min = '90';
|
|
230
|
+
elements.height.max = '220';
|
|
231
|
+
elements.height.step = '1';
|
|
232
|
+
elements.height.value = String(Math.round(state.heightCm));
|
|
233
|
+
elements.waist.min = '35';
|
|
234
|
+
elements.waist.max = '160';
|
|
235
|
+
elements.waist.step = '1';
|
|
236
|
+
elements.waist.value = String(Math.round(state.waistCm));
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function syncButtons(elements: WHtRElements, state: WHtRState) {
|
|
241
|
+
elements.unitBtns.forEach((btn) => btn.classList.toggle('whtr__unit-btn--active', btn.dataset.unit === state.unit));
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function render(elements: WHtRElements, state: WHtRState, ui: Record<string, string>) {
|
|
245
|
+
const ratio = state.waistCm / state.heightCm;
|
|
246
|
+
const rounded = Math.round(ratio * 100) / 100;
|
|
247
|
+
const level = classify(ratio);
|
|
248
|
+
const halfHeight = state.heightCm / 2;
|
|
249
|
+
const waistPosition = clamp((state.waistCm / state.heightCm) * 100, 18, 86);
|
|
250
|
+
const copy = {
|
|
251
|
+
veryThin: { label: ui.levelVeryThin, desc: ui.descVeryThin },
|
|
252
|
+
healthy: { label: ui.levelHealthy, desc: ui.descHealthy },
|
|
253
|
+
overweight: { label: ui.levelOverweight, desc: ui.descOverweight },
|
|
254
|
+
obese: { label: ui.levelObese, desc: ui.descObese },
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
elements.heightValue.textContent = formatLength(state.heightCm, state.unit, ui);
|
|
258
|
+
elements.waistValue.textContent = formatLength(state.waistCm, state.unit, ui);
|
|
259
|
+
elements.halfValue.textContent = formatLength(halfHeight, state.unit, ui);
|
|
260
|
+
elements.waistTag.textContent = formatLength(state.waistCm, state.unit, ui);
|
|
261
|
+
elements.safeLimit.textContent = formatLength(halfHeight, state.unit, ui);
|
|
262
|
+
elements.ratio.textContent = rounded.toFixed(2);
|
|
263
|
+
elements.level.textContent = copy[level].label;
|
|
264
|
+
elements.desc.textContent = copy[level].desc;
|
|
265
|
+
elements.panel.dataset.level = level;
|
|
266
|
+
elements.halfLine.style.top = '50%';
|
|
267
|
+
elements.waistLine.style.top = `${waistPosition}%`;
|
|
268
|
+
elements.waistBand.setAttribute('rx', String(clamp(28 + ratio * 30, 34, 55)));
|
|
269
|
+
elements.hipBand.setAttribute('rx', String(clamp(49 + ratio * 18, 54, 64)));
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function initWHtR(container: Element) {
|
|
273
|
+
const ui = JSON.parse((container as HTMLElement).dataset.ui || '{}') as Record<string, string>;
|
|
274
|
+
const elements = getElements(container);
|
|
275
|
+
if (!elements) return;
|
|
276
|
+
const state = loadState();
|
|
277
|
+
const update = () => {
|
|
278
|
+
state.heightCm = state.unit === 'imperial' ? inToCm(Number(elements.height.value)) : Number(elements.height.value);
|
|
279
|
+
state.waistCm = state.unit === 'imperial' ? inToCm(Number(elements.waist.value)) : Number(elements.waist.value);
|
|
280
|
+
render(elements, state, ui);
|
|
281
|
+
saveState(state);
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
elements.unitBtns.forEach((btn) => {
|
|
285
|
+
btn.addEventListener('click', () => {
|
|
286
|
+
state.unit = btn.dataset.unit as Unit;
|
|
287
|
+
syncButtons(elements, state);
|
|
288
|
+
syncRanges(elements, state);
|
|
289
|
+
update();
|
|
290
|
+
});
|
|
291
|
+
});
|
|
292
|
+
elements.height.addEventListener('input', update);
|
|
293
|
+
elements.waist.addEventListener('input', update);
|
|
294
|
+
syncButtons(elements, state);
|
|
295
|
+
syncRanges(elements, state);
|
|
296
|
+
update();
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
document.querySelectorAll('.whtr').forEach(initWHtR);
|
|
300
|
+
</script>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { HealthToolEntry } from '../../types';
|
|
2
|
+
import type { WaistHeightRatioCalculatorUI } from './ui';
|
|
3
|
+
|
|
4
|
+
export const waistHeightRatioCalculator: HealthToolEntry<WaistHeightRatioCalculatorUI> = {
|
|
5
|
+
id: 'waistHeightRatioCalculator',
|
|
6
|
+
icons: {
|
|
7
|
+
bg: 'mdi:tape-measure',
|
|
8
|
+
fg: 'mdi:human-male-height',
|
|
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
|
+
};
|