@jjlmoya/utils-health 1.19.0 → 1.20.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 +2 -0
  5. package/src/tests/locale_completeness.test.ts +1 -1
  6. package/src/tests/tool_validation.test.ts +1 -1
  7. package/src/tool/dailyProteinCalculator/bibliography.astro +14 -0
  8. package/src/tool/dailyProteinCalculator/bibliography.ts +28 -0
  9. package/src/tool/dailyProteinCalculator/component.astro +282 -0
  10. package/src/tool/dailyProteinCalculator/daily-protein-calculator.css +305 -0
  11. package/src/tool/dailyProteinCalculator/entry.ts +27 -0
  12. package/src/tool/dailyProteinCalculator/i18n/de.ts +353 -0
  13. package/src/tool/dailyProteinCalculator/i18n/en.ts +353 -0
  14. package/src/tool/dailyProteinCalculator/i18n/es.ts +353 -0
  15. package/src/tool/dailyProteinCalculator/i18n/fr.ts +353 -0
  16. package/src/tool/dailyProteinCalculator/i18n/id.ts +353 -0
  17. package/src/tool/dailyProteinCalculator/i18n/it.ts +353 -0
  18. package/src/tool/dailyProteinCalculator/i18n/ja.ts +353 -0
  19. package/src/tool/dailyProteinCalculator/i18n/ko.ts +353 -0
  20. package/src/tool/dailyProteinCalculator/i18n/nl.ts +353 -0
  21. package/src/tool/dailyProteinCalculator/i18n/pl.ts +353 -0
  22. package/src/tool/dailyProteinCalculator/i18n/pt.ts +353 -0
  23. package/src/tool/dailyProteinCalculator/i18n/ru.ts +353 -0
  24. package/src/tool/dailyProteinCalculator/i18n/sv.ts +358 -0
  25. package/src/tool/dailyProteinCalculator/i18n/tr.ts +87 -0
  26. package/src/tool/dailyProteinCalculator/i18n/zh.ts +328 -0
  27. package/src/tool/dailyProteinCalculator/index.ts +12 -0
  28. package/src/tool/dailyProteinCalculator/logic.ts +65 -0
  29. package/src/tool/dailyProteinCalculator/seo.astro +15 -0
  30. package/src/tool/dailyProteinCalculator/ui.ts +40 -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.19.0",
3
+ "version": "1.20.0",
4
4
  "type": "module",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -5,6 +5,7 @@ import { daltonismSimulator } from '../tool/daltonismSimulator/entry';
5
5
  import { tinnitusReliever } from '../tool/tinnitusReliever/entry';
6
6
  import { breathingVisualizer } from '../tool/breathingVisualizer/entry';
7
7
  import { caffeineTracker } from '../tool/caffeineTracker/entry';
8
+ import { dailyProteinCalculator } from '../tool/dailyProteinCalculator/entry';
8
9
  import { waterPurifier } from '../tool/waterPurifier/entry';
9
10
  import { digestionStopwatch } from '../tool/digestionStopwatch/entry';
10
11
  import { binauralTuner } from '../tool/binauralTuner/entry';
@@ -16,7 +17,7 @@ import { metabolicAgeCalculator } from '../tool/metabolicAgeCalculator/entry';
16
17
 
17
18
  export const healthCategory: HealthCategoryEntry = {
18
19
  icon: 'mdi:heart-pulse',
19
- tools: [bmiCalculator, bodyAdiposityIndexCalculator, leanBodyMassCalculator, metabolicAgeCalculator, daltonismSimulator, tinnitusReliever, breathingVisualizer, caffeineTracker, waterPurifier, digestionStopwatch, binauralTuner, pelliRobsonTest, peripheralVisionTrainer, epworthSleepinessScale],
20
+ tools: [bmiCalculator, bodyAdiposityIndexCalculator, leanBodyMassCalculator, metabolicAgeCalculator, dailyProteinCalculator, daltonismSimulator, tinnitusReliever, breathingVisualizer, caffeineTracker, waterPurifier, digestionStopwatch, binauralTuner, pelliRobsonTest, peripheralVisionTrainer, epworthSleepinessScale],
20
21
  i18n: {
21
22
  en: () => import('./i18n/en').then((m) => m.content),
22
23
  es: () => import('./i18n/es').then((m) => m.content),
package/src/entries.ts CHANGED
@@ -4,6 +4,7 @@ export { bmiCalculator } from './tool/bmiCalculator/entry';
4
4
  export { bodyAdiposityIndexCalculator } from './tool/bodyAdiposityIndexCalculator/entry';
5
5
  export { breathingVisualizer } from './tool/breathingVisualizer/entry';
6
6
  export { caffeineTracker } from './tool/caffeineTracker/entry';
7
+ export { dailyProteinCalculator } from './tool/dailyProteinCalculator/entry';
7
8
  export { daltonismSimulator } from './tool/daltonismSimulator/entry';
8
9
  export { digestionStopwatch } from './tool/digestionStopwatch/entry';
9
10
  export { epworthSleepinessScale } from './tool/epworthSleepinessScale/entry';
@@ -24,6 +25,7 @@ import { bmiCalculator } from './tool/bmiCalculator/entry';
24
25
  import { bodyAdiposityIndexCalculator } from './tool/bodyAdiposityIndexCalculator/entry';
25
26
  import { breathingVisualizer } from './tool/breathingVisualizer/entry';
26
27
  import { caffeineTracker } from './tool/caffeineTracker/entry';
28
+ import { dailyProteinCalculator } from './tool/dailyProteinCalculator/entry';
27
29
  import { daltonismSimulator } from './tool/daltonismSimulator/entry';
28
30
  import { digestionStopwatch } from './tool/digestionStopwatch/entry';
29
31
  import { epworthSleepinessScale } from './tool/epworthSleepinessScale/entry';
@@ -37,4 +39,4 @@ import { screenDecompressionTime } from './tool/screenDecompressionTime/entry';
37
39
  import { tinnitusReliever } from './tool/tinnitusReliever/entry';
38
40
  import { ubeCalculator } from './tool/ubeCalculator/entry';
39
41
  import { waterPurifier } from './tool/waterPurifier/entry';
40
- export const ALL_ENTRIES = [binauralTuner, bloodUnitConverter, bmiCalculator, bodyAdiposityIndexCalculator, breathingVisualizer, caffeineTracker, daltonismSimulator, digestionStopwatch, epworthSleepinessScale, hydrationCalculator, leanBodyMassCalculator, metabolicAgeCalculator, pelliRobsonTest, peripheralVisionTrainer, readingDistanceCalculator, screenDecompressionTime, tinnitusReliever, ubeCalculator, waterPurifier];
42
+ export const ALL_ENTRIES = [binauralTuner, bloodUnitConverter, bmiCalculator, bodyAdiposityIndexCalculator, breathingVisualizer, caffeineTracker, dailyProteinCalculator, daltonismSimulator, digestionStopwatch, epworthSleepinessScale, hydrationCalculator, leanBodyMassCalculator, metabolicAgeCalculator, pelliRobsonTest, peripheralVisionTrainer, readingDistanceCalculator, screenDecompressionTime, tinnitusReliever, ubeCalculator, waterPurifier];
package/src/index.ts CHANGED
@@ -15,6 +15,8 @@ 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/dailyProteinCalculator';
19
+ export * from './tool/leanBodyMassCalculator';
18
20
  export * from './tool/metabolicAgeCalculator';
19
21
 
20
22
  export { healthCategory } from './category';
@@ -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(19);
39
+ expect(ALL_TOOLS.length).toBe(20);
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(19);
8
+ expect(ALL_TOOLS.length).toBe(20);
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 { dailyProteinCalculator } 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 dailyProteinCalculator.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: 'Phillips SM, Chevalier S, Leidy HJ: Protein requirements beyond the RDA',
6
+ url: 'https://doi.org/10.1139/apnm-2015-0550',
7
+ },
8
+ {
9
+ name: 'Campbell B. et al.: International Society of Sports Nutrition position stand: protein and exercise',
10
+ url: 'https://doi.org/10.1186/1550-2783-4-8',
11
+ },
12
+ {
13
+ name: 'Jager R. et al.: International Society of Sports Nutrition position stand: protein and exercise update',
14
+ url: 'https://doi.org/10.1186/s12970-017-0177-8',
15
+ },
16
+ {
17
+ name: 'Institute of Medicine: Dietary Reference Intakes for Energy, Carbohydrate, Fiber, Fat, Fatty Acids, Cholesterol, Protein, and Amino Acids',
18
+ url: 'https://nap.nationalacademies.org/catalog/10490/dietary-reference-intakes-for-energy-carbohydrate-fiber-fat-fatty-acids-cholesterol-protein-and-amino-acids',
19
+ },
20
+ {
21
+ name: 'NIH Office of Dietary Supplements: Protein fact sheet',
22
+ url: 'https://ods.od.nih.gov/factsheets/Protein-HealthProfessional/',
23
+ },
24
+ {
25
+ name: 'Wikipedia: Protein in nutrition',
26
+ url: 'https://en.wikipedia.org/wiki/Protein_(nutrient)',
27
+ },
28
+ ];
@@ -0,0 +1,282 @@
1
+ ---
2
+ import { Icon } from 'astro-icon/components';
3
+ import type { DailyProteinCalculatorUI } from './ui';
4
+
5
+ interface Props {
6
+ locale?: string;
7
+ ui?: Record<string, unknown>;
8
+ }
9
+
10
+ const t = (Astro.props.ui || {}) as DailyProteinCalculatorUI;
11
+ ---
12
+
13
+ <div class="dpc" data-ui={JSON.stringify(t)}>
14
+ <div class="dpc__shell">
15
+ <section class="dpc__result" aria-live="polite">
16
+ <div class="dpc__plate" aria-hidden="true">
17
+ <div class="dpc__plate-ring"></div>
18
+ <Icon name="mdi:food-steak" />
19
+ </div>
20
+ <div class="dpc__target">
21
+ <span>{t.dailyTargetLabel}</span>
22
+ <strong id="dpc-total">132 g</strong>
23
+ </div>
24
+ <div class="dpc__stats">
25
+ <div><span>{t.rangeLabel}</span><strong id="dpc-range">112-151 g</strong></div>
26
+ <div><span>{t.ratioLabel}</span><strong id="dpc-ratio">1.8 g/kg</strong></div>
27
+ </div>
28
+ <div id="dpc-safe" class="dpc__safe" hidden>
29
+ <Icon name="mdi:alert-outline" />
30
+ <div><strong>{t.safeLimitTitle}</strong><p>{t.safeLimitText}</p></div>
31
+ </div>
32
+ </section>
33
+
34
+ <section class="dpc__controls">
35
+ <div class="dpc__topline">
36
+ <div class="dpc__seg" role="group" aria-label={t.unitSystemLabel}>
37
+ <button class="dpc__seg-btn dpc__seg-btn--active" type="button" data-unit="metric">{t.unitMetric}</button>
38
+ <button class="dpc__seg-btn" type="button" data-unit="imperial">{t.unitImperial}</button>
39
+ </div>
40
+ <label class="dpc__toggle">
41
+ <input id="dpc-pregnant" type="checkbox" />
42
+ <span>{t.pregnancyLabel}</span>
43
+ </label>
44
+ <label class="dpc__toggle">
45
+ <input id="dpc-lactating" type="checkbox" />
46
+ <span>{t.lactationLabel}</span>
47
+ </label>
48
+ </div>
49
+
50
+ <div class="dpc__field">
51
+ <label for="dpc-weight"><b>{t.weightLabel}</b><output id="dpc-weight-output">75 kg</output></label>
52
+ <input id="dpc-weight" class="dpc__range" type="range" min="40" max="180" value="75" />
53
+ </div>
54
+
55
+ <div class="dpc__field">
56
+ <label for="dpc-age"><b>{t.ageLabel}</b><output id="dpc-age-output">38 years</output></label>
57
+ <input id="dpc-age" class="dpc__range" type="range" min="16" max="90" value="38" />
58
+ </div>
59
+
60
+ <div>
61
+ <span class="dpc__label">{t.goalLabel}</span>
62
+ <div class="dpc__chips" role="group" aria-label={t.goalLabel}>
63
+ <button class="dpc__chip" type="button" data-goal="fatLoss">{t.goalFatLoss}</button>
64
+ <button class="dpc__chip dpc__chip--active" type="button" data-goal="maintenance">{t.goalMaintenance}</button>
65
+ <button class="dpc__chip" type="button" data-goal="muscleGain">{t.goalMuscleGain}</button>
66
+ <button class="dpc__chip" type="button" data-goal="recovery">{t.goalRecovery}</button>
67
+ </div>
68
+ </div>
69
+
70
+ <div>
71
+ <span class="dpc__label">{t.activityLabel}</span>
72
+ <div class="dpc__chips" role="group" aria-label={t.activityLabel}>
73
+ <button class="dpc__chip" type="button" data-activity="sedentary">{t.activitySedentary}</button>
74
+ <button class="dpc__chip dpc__chip--active" type="button" data-activity="active">{t.activityActive}</button>
75
+ <button class="dpc__chip" type="button" data-activity="endurance">{t.activityEndurance}</button>
76
+ <button class="dpc__chip" type="button" data-activity="strength">{t.activityStrength}</button>
77
+ <button class="dpc__chip" type="button" data-activity="athlete">{t.activityAthlete}</button>
78
+ </div>
79
+ </div>
80
+ </section>
81
+ <section class="dpc__foods">
82
+ <h3>{t.foodPanelTitle}</h3>
83
+ <div class="dpc__food-grid">
84
+ <div><Icon name="mdi:food-drumstick" /><span>{t.chickenLabel}</span><strong id="dpc-chicken">120 g</strong></div>
85
+ <div><Icon name="mdi:soy-sauce" /><span>{t.tofuLabel}</span><strong id="dpc-tofu">310 g</strong></div>
86
+ <div><Icon name="mdi:egg" /><span>{t.eggsLabel}</span><strong id="dpc-eggs">5</strong></div>
87
+ <div><Icon name="mdi:bowl-mix" /><span>{t.greekYogurtLabel}</span><strong id="dpc-yogurt">270 g</strong></div>
88
+ <div><Icon name="mdi:spoon-sugar" /><span>{t.proteinPowderLabel}</span><strong id="dpc-powder">5 {t.scoops}</strong></div>
89
+ <div><Icon name="mdi:fish" /><span>{t.tunaLabel}</span><strong id="dpc-tuna">120 {t.grams}</strong></div>
90
+ </div>
91
+ <p><strong>{t.educationTitle}</strong> {t.educationText}</p>
92
+ </section>
93
+ </div>
94
+ </div>
95
+
96
+ <script>
97
+ type Unit = 'metric' | 'imperial';
98
+ type Goal = 'fatLoss' | 'maintenance' | 'muscleGain' | 'recovery';
99
+ type Activity = 'sedentary' | 'active' | 'endurance' | 'strength' | 'athlete';
100
+
101
+ interface CalculatorState {
102
+ unit: Unit;
103
+ weightKg: number;
104
+ age: number;
105
+ goal: Goal;
106
+ activity: Activity;
107
+ pregnant: boolean;
108
+ lactating: boolean;
109
+ }
110
+
111
+ const activityRanges: Record<Activity, [number, number]> = {
112
+ sedentary: [0.8, 1],
113
+ active: [1.2, 1.5],
114
+ endurance: [1.2, 1.6],
115
+ strength: [1.6, 2.2],
116
+ athlete: [1.8, 2.3],
117
+ };
118
+ const goalBoost: Record<Goal, [number, number]> = {
119
+ maintenance: [0, 0],
120
+ recovery: [0.2, 0.3],
121
+ muscleGain: [0.2, 0.4],
122
+ fatLoss: [0.5, 0.7],
123
+ };
124
+
125
+ function requireEl<T extends Element>(root: Element, selector: string): T {
126
+ const el = root.querySelector<T>(selector);
127
+ if (!el) throw new Error(`Missing ${selector}`);
128
+ return el;
129
+ }
130
+
131
+ function kgToLb(kg: number) { return kg * 2.2046226218; }
132
+ function lbToKg(lb: number) { return lb / 2.2046226218; }
133
+
134
+ function calculate(state: CalculatorState) {
135
+ const [activityLow, activityHigh] = activityRanges[state.activity];
136
+ const [boostLow, boostHigh] = goalBoost[state.goal];
137
+ const ageBoost = state.age >= 65 ? 0.2 : 0;
138
+ const low = Math.min(2.4, activityLow + boostLow + ageBoost);
139
+ const high = Math.min(2.6, activityHigh + boostHigh + ageBoost);
140
+ const ratio = Math.round(((low + high) / 2) * 10) / 10;
141
+ const added = (state.pregnant ? 25 : 0) + (state.lactating ? 20 : 0);
142
+ const total = Math.round(state.weightKg * ratio + added);
143
+ return { total, low: Math.round(state.weightKg * low + added), high: Math.round(state.weightKg * high + added), ratio };
144
+ }
145
+
146
+ function syncWeightControl(state: CalculatorState, weight: HTMLInputElement) {
147
+ if (state.unit === 'imperial') {
148
+ weight.min = '88';
149
+ weight.max = '397';
150
+ weight.value = String(Math.round(kgToLb(state.weightKg)));
151
+ } else {
152
+ weight.min = '40';
153
+ weight.max = '180';
154
+ weight.value = String(Math.round(state.weightKg));
155
+ }
156
+ }
157
+
158
+ function renderFoods(root: Element, total: number, ui: Record<string, string>) {
159
+ requireEl<HTMLElement>(root, '#dpc-chicken').textContent = `${Math.round((total / 31) * 100)} ${ui.grams}`;
160
+ requireEl<HTMLElement>(root, '#dpc-tofu').textContent = `${Math.round((total / 12) * 100)} ${ui.grams}`;
161
+ requireEl<HTMLElement>(root, '#dpc-eggs').textContent = `${Math.max(1, Math.round(total / 6))}`;
162
+ requireEl<HTMLElement>(root, '#dpc-yogurt').textContent = `${Math.round((total / 10) * 100)} ${ui.grams}`;
163
+ requireEl<HTMLElement>(root, '#dpc-powder').textContent = `${(total / 25).toFixed(1)} ${ui.scoops}`;
164
+ requireEl<HTMLElement>(root, '#dpc-tuna').textContent = `${Math.round((total / 26) * 100)} ${ui.grams}`;
165
+ }
166
+
167
+ function loadSavedState(state: CalculatorState) {
168
+ const saved = localStorage.getItem('dpc_state');
169
+ if (!saved) return;
170
+ try {
171
+ const parsed = JSON.parse(saved);
172
+ if (parsed.unit) state.unit = parsed.unit;
173
+ if (parsed.weightKg) state.weightKg = parsed.weightKg;
174
+ if (parsed.age) state.age = parsed.age;
175
+ if (parsed.goal) state.goal = parsed.goal;
176
+ if (parsed.activity) state.activity = parsed.activity;
177
+ state.pregnant = !!parsed.pregnant;
178
+ state.lactating = !!parsed.lactating;
179
+ } catch {}
180
+ }
181
+
182
+ function setupInputs(root: Element, state: CalculatorState) {
183
+ const weight = requireEl<HTMLInputElement>(root, '#dpc-weight');
184
+ const age = requireEl<HTMLInputElement>(root, '#dpc-age');
185
+ const pregnant = requireEl<HTMLInputElement>(root, '#dpc-pregnant');
186
+ const lactating = requireEl<HTMLInputElement>(root, '#dpc-lactating');
187
+
188
+ weight.value = String(state.unit === 'imperial' ? Math.round(kgToLb(state.weightKg)) : Math.round(state.weightKg));
189
+ age.value = String(state.age);
190
+ pregnant.checked = state.pregnant;
191
+ lactating.checked = state.lactating;
192
+ }
193
+
194
+ function updateActiveClasses(root: Element, state: CalculatorState) {
195
+ root.querySelectorAll<HTMLButtonElement>('[data-unit]').forEach((item) => {
196
+ item.classList.toggle('dpc__seg-btn--active', item.dataset.unit === state.unit);
197
+ });
198
+ root.querySelectorAll<HTMLButtonElement>('[data-goal]').forEach((item) => {
199
+ item.classList.toggle('dpc__chip--active', item.dataset.goal === state.goal);
200
+ });
201
+ root.querySelectorAll<HTMLButtonElement>('[data-activity]').forEach((item) => {
202
+ item.classList.toggle('dpc__chip--active', item.dataset.activity === state.activity);
203
+ });
204
+ }
205
+
206
+ function renderAll(root: Element, state: CalculatorState, ui: Record<string, string>) {
207
+ const result = calculate(state);
208
+ requireEl<HTMLElement>(root, '#dpc-total').textContent = `${result.total} ${ui.grams}`;
209
+ requireEl<HTMLElement>(root, '#dpc-range').textContent = `${result.low}-${result.high} ${ui.grams}`;
210
+ requireEl<HTMLElement>(root, '#dpc-ratio').textContent = `${result.ratio.toFixed(1)} g/kg`;
211
+ requireEl<HTMLOutputElement>(root, '#dpc-weight-output').textContent = state.unit === 'imperial' ? `${Math.round(kgToLb(state.weightKg))} ${ui.lb}` : `${Math.round(state.weightKg)} ${ui.kg}`;
212
+ requireEl<HTMLOutputElement>(root, '#dpc-age-output').textContent = `${state.age} ${ui.years}`;
213
+ requireEl<HTMLElement>(root, '#dpc-safe').hidden = result.total / state.weightKg <= 2.5;
214
+ renderFoods(root, result.total, ui);
215
+ localStorage.setItem('dpc_state', JSON.stringify(state));
216
+ }
217
+
218
+ function bindButtons(root: Element, state: CalculatorState, ui: Record<string, string>, weight: HTMLInputElement) {
219
+ root.querySelectorAll<HTMLButtonElement>('[data-unit]').forEach((btn) => {
220
+ btn.addEventListener('click', () => {
221
+ state.unit = btn.dataset.unit as Unit;
222
+ updateActiveClasses(root, state);
223
+ syncWeightControl(state, weight);
224
+ renderAll(root, state, ui);
225
+ });
226
+ });
227
+ root.querySelectorAll<HTMLButtonElement>('[data-goal]').forEach((btn) => {
228
+ btn.addEventListener('click', () => {
229
+ state.goal = btn.dataset.goal as Goal;
230
+ updateActiveClasses(root, state);
231
+ renderAll(root, state, ui);
232
+ });
233
+ });
234
+ root.querySelectorAll<HTMLButtonElement>('[data-activity]').forEach((btn) => {
235
+ btn.addEventListener('click', () => {
236
+ state.activity = btn.dataset.activity as Activity;
237
+ updateActiveClasses(root, state);
238
+ renderAll(root, state, ui);
239
+ });
240
+ });
241
+ }
242
+
243
+ function bindInputs(root: Element, state: CalculatorState, ui: Record<string, string>, weight: HTMLInputElement) {
244
+ const age = requireEl<HTMLInputElement>(root, '#dpc-age');
245
+ const pregnant = requireEl<HTMLInputElement>(root, '#dpc-pregnant');
246
+ const lactating = requireEl<HTMLInputElement>(root, '#dpc-lactating');
247
+
248
+ weight.addEventListener('input', () => {
249
+ state.weightKg = state.unit === 'imperial' ? lbToKg(Number(weight.value)) : Number(weight.value);
250
+ renderAll(root, state, ui);
251
+ });
252
+ age.addEventListener('input', () => {
253
+ state.age = Number(age.value);
254
+ renderAll(root, state, ui);
255
+ });
256
+ pregnant.addEventListener('change', () => {
257
+ state.pregnant = pregnant.checked;
258
+ renderAll(root, state, ui);
259
+ });
260
+ lactating.addEventListener('change', () => {
261
+ state.lactating = lactating.checked;
262
+ renderAll(root, state, ui);
263
+ });
264
+ }
265
+
266
+ function initDailyProtein(root: Element) {
267
+ const ui = JSON.parse((root as HTMLElement).dataset.ui || '{}') as Record<string, string>;
268
+ const state: CalculatorState = { unit: 'metric', weightKg: 75, age: 38, goal: 'maintenance', activity: 'active', pregnant: false, lactating: false };
269
+ const weight = requireEl<HTMLInputElement>(root, '#dpc-weight');
270
+
271
+ loadSavedState(state);
272
+ setupInputs(root, state);
273
+ updateActiveClasses(root, state);
274
+ syncWeightControl(state, weight);
275
+ bindButtons(root, state, ui, weight);
276
+ bindInputs(root, state, ui, weight);
277
+ renderAll(root, state, ui);
278
+ }
279
+
280
+ document.querySelectorAll('.dpc').forEach(initDailyProtein);
281
+ </script>
282
+