@jjlmoya/utils-health 1.35.0 → 1.36.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/entries.ts +3 -1
  4. package/src/index.ts +1 -0
  5. package/src/tests/locale_completeness.test.ts +1 -1
  6. package/src/tests/tool_validation.test.ts +2 -2
  7. package/src/tool/creatinineClearanceCockcroftGault/bibliography.astro +14 -0
  8. package/src/tool/creatinineClearanceCockcroftGault/bibliography.ts +28 -0
  9. package/src/tool/creatinineClearanceCockcroftGault/client.ts +149 -0
  10. package/src/tool/creatinineClearanceCockcroftGault/component.astro +147 -0
  11. package/src/tool/creatinineClearanceCockcroftGault/creatinine-clearance-cockcroft-gault.css +550 -0
  12. package/src/tool/creatinineClearanceCockcroftGault/entry.ts +27 -0
  13. package/src/tool/creatinineClearanceCockcroftGault/i18n/de.ts +306 -0
  14. package/src/tool/creatinineClearanceCockcroftGault/i18n/en.ts +306 -0
  15. package/src/tool/creatinineClearanceCockcroftGault/i18n/es.ts +306 -0
  16. package/src/tool/creatinineClearanceCockcroftGault/i18n/fr.ts +306 -0
  17. package/src/tool/creatinineClearanceCockcroftGault/i18n/id.ts +306 -0
  18. package/src/tool/creatinineClearanceCockcroftGault/i18n/it.ts +306 -0
  19. package/src/tool/creatinineClearanceCockcroftGault/i18n/ja.ts +306 -0
  20. package/src/tool/creatinineClearanceCockcroftGault/i18n/ko.ts +306 -0
  21. package/src/tool/creatinineClearanceCockcroftGault/i18n/nl.ts +306 -0
  22. package/src/tool/creatinineClearanceCockcroftGault/i18n/pl.ts +306 -0
  23. package/src/tool/creatinineClearanceCockcroftGault/i18n/pt.ts +306 -0
  24. package/src/tool/creatinineClearanceCockcroftGault/i18n/ru.ts +306 -0
  25. package/src/tool/creatinineClearanceCockcroftGault/i18n/sv.ts +306 -0
  26. package/src/tool/creatinineClearanceCockcroftGault/i18n/tr.ts +306 -0
  27. package/src/tool/creatinineClearanceCockcroftGault/i18n/zh.ts +306 -0
  28. package/src/tool/creatinineClearanceCockcroftGault/index.ts +12 -0
  29. package/src/tool/creatinineClearanceCockcroftGault/logic.ts +99 -0
  30. package/src/tool/creatinineClearanceCockcroftGault/seo.astro +15 -0
  31. package/src/tool/creatinineClearanceCockcroftGault/ui.ts +59 -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.35.0",
3
+ "version": "1.36.0",
4
4
  "type": "module",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -31,10 +31,11 @@ import { targetHeightEstimator } from '../tool/targetHeightEstimator/entry';
31
31
  import { correctedReticulocyteIndex } from '../tool/correctedReticulocyteIndex/entry';
32
32
  import { estimatedBloodVolumeCalculator } from '../tool/estimatedBloodVolumeCalculator/entry';
33
33
  import { anionGapCalculator } from '../tool/anionGapCalculator/entry';
34
+ import { creatinineClearanceCockcroftGault } from '../tool/creatinineClearanceCockcroftGault/entry';
34
35
 
35
36
  export const healthCategory: HealthCategoryEntry = {
36
37
  icon: 'mdi:heart-pulse',
37
- tools: [bmiCalculator, amputationAdjustedBmiCalculator, bodyAdiposityIndexCalculator, bodyFatPercentageCalculator, waistHipRatioCalculator, waistHeightRatioCalculator, leanBodyMassCalculator, metabolicAgeCalculator, caloricDeficitCalculator, dailyProteinCalculator, macronutrientCalculator, tdeeCalculator, bunCreatinineCalculator, serumOsmolalityCalculator, meanArterialPressureCalculator, correctedSodiumHyperglycemiaCalculator, fractionalExcretionSodiumCalculator, correctedReticulocyteIndex, estimatedBloodVolumeCalculator, anionGapCalculator, targetHeightEstimator, daltonismSimulator, tinnitusReliever, parametricNoiseSynthesizer, breathingVisualizer, caffeineTracker, waterPurifier, digestionStopwatch, binauralTuner, pelliRobsonTest, peripheralVisionTrainer, epworthSleepinessScale],
38
+ tools: [bmiCalculator, amputationAdjustedBmiCalculator, bodyAdiposityIndexCalculator, bodyFatPercentageCalculator, waistHipRatioCalculator, waistHeightRatioCalculator, leanBodyMassCalculator, metabolicAgeCalculator, caloricDeficitCalculator, dailyProteinCalculator, macronutrientCalculator, tdeeCalculator, bunCreatinineCalculator, serumOsmolalityCalculator, meanArterialPressureCalculator, correctedSodiumHyperglycemiaCalculator, fractionalExcretionSodiumCalculator, correctedReticulocyteIndex, estimatedBloodVolumeCalculator, anionGapCalculator, creatinineClearanceCockcroftGault, targetHeightEstimator, daltonismSimulator, tinnitusReliever, parametricNoiseSynthesizer, breathingVisualizer, caffeineTracker, waterPurifier, digestionStopwatch, binauralTuner, pelliRobsonTest, peripheralVisionTrainer, epworthSleepinessScale],
38
39
  i18n: {
39
40
  en: () => import('./i18n/en').then((m) => m.content),
40
41
  es: () => import('./i18n/es').then((m) => m.content),
package/src/entries.ts CHANGED
@@ -27,6 +27,7 @@ export { targetHeightEstimator } from './tool/targetHeightEstimator/entry';
27
27
  export { correctedReticulocyteIndex } from './tool/correctedReticulocyteIndex/entry';
28
28
  export { estimatedBloodVolumeCalculator } from './tool/estimatedBloodVolumeCalculator/entry';
29
29
  export { anionGapCalculator } from './tool/anionGapCalculator/entry';
30
+ export { creatinineClearanceCockcroftGault } from './tool/creatinineClearanceCockcroftGault/entry';
30
31
  export { pelliRobsonTest } from './tool/pelliRobsonTest/entry';
31
32
  export { peripheralVisionTrainer } from './tool/peripheralVisionTrainer/entry';
32
33
  export { readingDistanceCalculator } from './tool/readingDistanceCalculator/entry';
@@ -65,6 +66,7 @@ import { targetHeightEstimator } from './tool/targetHeightEstimator/entry';
65
66
  import { correctedReticulocyteIndex } from './tool/correctedReticulocyteIndex/entry';
66
67
  import { estimatedBloodVolumeCalculator } from './tool/estimatedBloodVolumeCalculator/entry';
67
68
  import { anionGapCalculator } from './tool/anionGapCalculator/entry';
69
+ import { creatinineClearanceCockcroftGault } from './tool/creatinineClearanceCockcroftGault/entry';
68
70
  import { pelliRobsonTest } from './tool/pelliRobsonTest/entry';
69
71
  import { peripheralVisionTrainer } from './tool/peripheralVisionTrainer/entry';
70
72
  import { readingDistanceCalculator } from './tool/readingDistanceCalculator/entry';
@@ -73,4 +75,4 @@ import { tinnitusReliever } from './tool/tinnitusReliever/entry';
73
75
  import { parametricNoiseSynthesizer } from './tool/parametricNoiseSynthesizer/entry';
74
76
  import { ubeCalculator } from './tool/ubeCalculator/entry';
75
77
  import { waterPurifier } from './tool/waterPurifier/entry';
76
- export const ALL_ENTRIES = [binauralTuner, bloodUnitConverter, bmiCalculator, amputationAdjustedBmiCalculator, bodyAdiposityIndexCalculator, bodyFatPercentageCalculator, waistHipRatioCalculator, waistHeightRatioCalculator, breathingVisualizer, caffeineTracker, caloricDeficitCalculator, dailyProteinCalculator, macronutrientCalculator, tdeeCalculator, bunCreatinineCalculator, serumOsmolalityCalculator, meanArterialPressureCalculator, correctedSodiumHyperglycemiaCalculator, fractionalExcretionSodiumCalculator, correctedReticulocyteIndex, estimatedBloodVolumeCalculator, anionGapCalculator, targetHeightEstimator, daltonismSimulator, digestionStopwatch, epworthSleepinessScale, hydrationCalculator, leanBodyMassCalculator, metabolicAgeCalculator, pelliRobsonTest, peripheralVisionTrainer, readingDistanceCalculator, screenDecompressionTime, tinnitusReliever, parametricNoiseSynthesizer, ubeCalculator, waterPurifier];
78
+ export const ALL_ENTRIES = [binauralTuner, bloodUnitConverter, bmiCalculator, amputationAdjustedBmiCalculator, bodyAdiposityIndexCalculator, bodyFatPercentageCalculator, waistHipRatioCalculator, waistHeightRatioCalculator, breathingVisualizer, caffeineTracker, caloricDeficitCalculator, dailyProteinCalculator, macronutrientCalculator, tdeeCalculator, bunCreatinineCalculator, serumOsmolalityCalculator, meanArterialPressureCalculator, correctedSodiumHyperglycemiaCalculator, fractionalExcretionSodiumCalculator, correctedReticulocyteIndex, estimatedBloodVolumeCalculator, anionGapCalculator, creatinineClearanceCockcroftGault, targetHeightEstimator, daltonismSimulator, digestionStopwatch, epworthSleepinessScale, hydrationCalculator, leanBodyMassCalculator, metabolicAgeCalculator, pelliRobsonTest, peripheralVisionTrainer, readingDistanceCalculator, screenDecompressionTime, tinnitusReliever, parametricNoiseSynthesizer, ubeCalculator, waterPurifier];
package/src/index.ts CHANGED
@@ -35,6 +35,7 @@ export * from './tool/targetHeightEstimator';
35
35
  export * from './tool/correctedReticulocyteIndex';
36
36
  export * from './tool/estimatedBloodVolumeCalculator';
37
37
  export * from './tool/anionGapCalculator';
38
+ export * from './tool/creatinineClearanceCockcroftGault';
38
39
 
39
40
  export { healthCategory } from './category';
40
41
  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 tools registered', () => {
39
- expect(ALL_TOOLS.length).toBe(37);
39
+ expect(ALL_TOOLS.length).toBe(38);
40
40
  });
41
41
  });
@@ -4,8 +4,8 @@ import { healthCategory } from '../data';
4
4
 
5
5
  describe('Tool Validation Suite', () => {
6
6
  describe('Library Registration', () => {
7
- it('should have 37 tools in ALL_TOOLS', () => {
8
- expect(ALL_TOOLS.length).toBe(37);
7
+ it('should have 38 tools in ALL_TOOLS', () => {
8
+ expect(ALL_TOOLS.length).toBe(38);
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 { creatinineClearanceCockcroftGault } 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 creatinineClearanceCockcroftGault.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: 'Cockcroft DW, Gault MH. Prediction of creatinine clearance from serum creatinine. Nephron. 1976.',
6
+ url: 'https://pubmed.ncbi.nlm.nih.gov/1244564/',
7
+ },
8
+ {
9
+ name: 'Spinler SA, et al. Systematic evaluation of creatinine clearance in obese patients. Annals of Pharmacotherapy. 1998.',
10
+ url: 'https://pubmed.ncbi.nlm.nih.gov/9659710/',
11
+ },
12
+ {
13
+ name: 'Devine BJ. Gentamicin therapy. Drug Intelligence and Clinical Pharmacy. 1974.',
14
+ url: 'https://pubmed.ncbi.nlm.nih.gov/4840434/',
15
+ },
16
+ {
17
+ name: 'National Kidney Foundation: Estimated Glomerular Filtration Rate (eGFR)',
18
+ url: 'https://www.kidney.org/kidney-topics/estimated-glomerular-filtration-rate-egfr',
19
+ },
20
+ {
21
+ name: 'KDIGO 2024 Clinical Practice Guideline for the Evaluation and Management of Chronic Kidney Disease',
22
+ url: 'https://kdigo.org/guidelines/ckd-evaluation-and-management/',
23
+ },
24
+ {
25
+ name: 'Wikipedia: Cockcroft-Gault equation',
26
+ url: 'https://en.wikipedia.org/wiki/Creatinine_clearance#Cockcroft-Gault_formula',
27
+ },
28
+ ];
@@ -0,0 +1,149 @@
1
+ type Sex = 'male' | 'female';
2
+ type WeightMode = 'actual' | 'ideal' | 'adjusted';
3
+ type UnitSystem = 'metric' | 'imperial';
4
+ type CreatinineUnit = 'mg/dL' | 'umol/L';
5
+ type Stage = 'normal' | 'mild' | 'moderate' | 'severe' | 'failure';
6
+ type Store = Partial<Record<'sex' | 'weightMode' | 'unitSystem' | 'creatinineUnit' | 'age' | 'weight' | 'heightCm' | 'heightFt' | 'heightIn' | 'creatinine', string>>;
7
+ interface Ctx {
8
+ root: Element; ui: Record<string, string>; sex: Sex; weightMode: WeightMode; unitSystem: UnitSystem; creatinineUnit: CreatinineUnit;
9
+ age: HTMLInputElement; ageRange: HTMLInputElement; weight: HTMLInputElement; weightRange: HTMLInputElement;
10
+ heightCm: HTMLInputElement; heightFt: HTMLInputElement; heightIn: HTMLInputElement; heightRange: HTMLInputElement;
11
+ weightUnitLabel: HTMLElement; heightUnitLabel: HTMLElement; heightPair: HTMLElement; creatinine: HTMLInputElement; creatinineRange: HTMLInputElement;
12
+ }
13
+ const key = 'health:creatinine-clearance-cockcroft-gault:v1';
14
+ const round = (value: number) => Math.round(value * 10) / 10;
15
+ const kgToLb = (value: number) => value * 2.2046226218;
16
+ const lbToKg = (value: number) => value / 2.2046226218;
17
+ const mgToUmol = (value: number) => value * 88.4;
18
+ const umolToMg = (value: number) => value / 88.4;
19
+ const num = (input: HTMLInputElement) => Math.max(0, parseFloat(input.value) || 0);
20
+ function qs<T extends HTMLElement>(root: Element, selector: string): T {
21
+ const el = root.querySelector<T>(selector);
22
+ if (!el) throw new Error(`Missing ${selector}`);
23
+ return el;
24
+ }
25
+ function press(root: Element, selector: string, active: HTMLButtonElement, className: string) {
26
+ root.querySelectorAll<HTMLButtonElement>(selector).forEach((button) => button.classList.toggle(className, button === active));
27
+ }
28
+ const heightCm = (ctx: Ctx) => (ctx.unitSystem === 'imperial' ? (num(ctx.heightFt) * 12 + num(ctx.heightIn)) * 2.54 : num(ctx.heightCm));
29
+ const actualKg = (ctx: Ctx) => (ctx.unitSystem === 'imperial' ? lbToKg(num(ctx.weight)) : num(ctx.weight));
30
+ const creatinineMgDl = (ctx: Ctx) => (ctx.creatinineUnit === 'umol/L' ? umolToMg(num(ctx.creatinine)) : num(ctx.creatinine));
31
+ const ibw = (sex: Sex, cm: number) => round((sex === 'male' ? 50 : 45.5) + 2.3 * Math.max(0, cm / 2.54 - 60));
32
+ const clearance = (sex: Sex, age: number, weightKg: number, scr: number) => round((((140 - age) * weightKg) / (72 * Math.max(0.1, scr))) * (sex === 'female' ? 0.85 : 1));
33
+ function stage(value: number): Stage {
34
+ if (value >= 90) return 'normal';
35
+ if (value >= 60) return 'mild';
36
+ if (value >= 30) return 'moderate';
37
+ if (value >= 15) return 'severe';
38
+ return 'failure';
39
+ }
40
+ function marker(value: number) {
41
+ if (value >= 90) return Math.min(97, 72 + ((Math.min(value, 180) - 90) / 90) * 25);
42
+ if (value < 15) return Math.max(2, (value / 15) * 12);
43
+ if (value < 30) return 12 + ((value - 15) / 15) * 12;
44
+ if (value < 60) return 24 + ((value - 30) / 30) * 24;
45
+ return 48 + ((value - 60) / 30) * 24;
46
+ }
47
+ function clearText(value: number) {
48
+ if (value >= 10000) return `${Math.round(value / 100) / 10}k`;
49
+ if (value >= 1000) return String(Math.round(value));
50
+ return String(value);
51
+ }
52
+ function sizeClass(text: string) {
53
+ if (text.length >= 5) return 'cgx__value-number--xs';
54
+ if (text.length >= 4) return 'cgx__value-number--sm';
55
+ return '';
56
+ }
57
+ function weightText(ctx: Ctx, kg: number) {
58
+ return ctx.unitSystem === 'imperial' ? `${round(kgToLb(kg))} ${ctx.ui.lbUnit} (${round(kg)} ${ctx.ui.kgUnit})` : `${round(kg)} ${ctx.ui.kgUnit}`;
59
+ }
60
+ function calcWeight(mode: WeightMode, actual: number, ideal: number, adjusted: number) {
61
+ if (mode === 'ideal') return ideal;
62
+ if (mode === 'adjusted') return adjusted;
63
+ return actual;
64
+ }
65
+ function syncRanges(ctx: Ctx) {
66
+ ctx.ageRange.value = String(Math.min(100, Math.max(18, num(ctx.age))));
67
+ ctx.weightRange.value = String(Math.min(180, Math.max(35, actualKg(ctx))));
68
+ ctx.heightRange.value = String(Math.min(215, Math.max(135, heightCm(ctx))));
69
+ ctx.creatinineRange.value = String(Math.min(6, Math.max(0.3, creatinineMgDl(ctx))));
70
+ }
71
+ function setUnitSystem(ctx: Ctx, next: UnitSystem) {
72
+ if (next === ctx.unitSystem) return;
73
+ const weightKg = actualKg(ctx), cm = heightCm(ctx), inches = cm / 2.54;
74
+ ctx.unitSystem = next; ctx.weight.value = String(round(next === 'imperial' ? kgToLb(weightKg) : weightKg));
75
+ ctx.heightCm.value = String(round(cm)); ctx.heightFt.value = String(Math.floor(inches / 12)); ctx.heightIn.value = String(round(inches % 12));
76
+ applyUnits(ctx);
77
+ }
78
+ function applyUnits(ctx: Ctx) {
79
+ ctx.heightCm.hidden = ctx.unitSystem === 'imperial'; ctx.heightPair.hidden = ctx.unitSystem !== 'imperial';
80
+ ctx.weightUnitLabel.textContent = ctx.unitSystem === 'imperial' ? ctx.ui.lbUnit : ctx.ui.kgUnit;
81
+ ctx.heightUnitLabel.textContent = ctx.unitSystem === 'imperial' ? ctx.ui.ftInUnit : ctx.ui.cmUnit;
82
+ }
83
+ function save(ctx: Ctx) {
84
+ try {
85
+ const state = { sex: ctx.sex, weightMode: ctx.weightMode, unitSystem: ctx.unitSystem, creatinineUnit: ctx.creatinineUnit, age: ctx.age.value, weight: ctx.weight.value, heightCm: ctx.heightCm.value, heightFt: ctx.heightFt.value, heightIn: ctx.heightIn.value, creatinine: ctx.creatinine.value };
86
+ window.localStorage.setItem(key, JSON.stringify(state));
87
+ } catch {}
88
+ }
89
+ function applyStore(ctx: Ctx, state: Store) {
90
+ if (['male', 'female'].includes(String(state.sex))) ctx.sex = state.sex as Sex;
91
+ if (['actual', 'ideal', 'adjusted'].includes(String(state.weightMode))) ctx.weightMode = state.weightMode as WeightMode;
92
+ if (['metric', 'imperial'].includes(String(state.unitSystem))) ctx.unitSystem = state.unitSystem as UnitSystem;
93
+ if (['mg/dL', 'umol/L'].includes(String(state.creatinineUnit))) ctx.creatinineUnit = state.creatinineUnit as CreatinineUnit;
94
+ (['age', 'weight', 'heightCm', 'heightFt', 'heightIn', 'creatinine'] as const).forEach((name) => { if (typeof state[name] === 'string') ctx[name].value = state[name]; });
95
+ }
96
+ function restore(ctx: Ctx) {
97
+ try {
98
+ const raw = window.localStorage.getItem(key);
99
+ if (!raw) return;
100
+ applyStore(ctx, JSON.parse(raw) as Store); applyUnits(ctx);
101
+ active(ctx, { selector: '[data-sex]', dataKey: 'sex', value: ctx.sex, className: 'is-on' }); active(ctx, { selector: '[data-weight-mode]', dataKey: 'weightMode', value: ctx.weightMode, className: 'is-picked' });
102
+ active(ctx, { selector: '[data-unit-system]', dataKey: 'unitSystem', value: ctx.unitSystem, className: 'is-on' }); active(ctx, { selector: '[data-creatinine-unit]', dataKey: 'creatinineUnit', value: ctx.creatinineUnit, className: 'is-on' });
103
+ } catch {}
104
+ }
105
+ function active(ctx: Ctx, config: { selector: string; dataKey: string; value: string; className: string }) {
106
+ ctx.root.querySelectorAll<HTMLButtonElement>(config.selector).forEach((button) => button.classList.toggle(config.className, button.dataset[config.dataKey] === config.value));
107
+ }
108
+ function renderGauge(ctx: Ctx, value: number, valueStage: Stage) {
109
+ const text = clearText(value), el = qs<HTMLElement>(ctx.root, '#cgx-clearance'), valueSize = sizeClass(text);
110
+ el.textContent = text; el.classList.remove('cgx__value-number--sm', 'cgx__value-number--xs'); if (valueSize) el.classList.add(valueSize);
111
+ qs<HTMLElement>(ctx.root, '#cgx-marker').style.left = `${marker(value)}%`; qs<HTMLElement>(ctx.root, '#cgx-stage-pill').textContent = ctx.ui[`${valueStage}Label`];
112
+ qs<HTMLElement>(ctx.root, '#cgx-stage-body').textContent = ctx.ui[`${valueStage}Body`]; qs<HTMLElement>(ctx.root, '.cgx__gauge').dataset.stage = valueStage;
113
+ }
114
+ function renderWeights(ctx: Ctx, values: { actual: number; ideal: number; adjusted: number; age: number; scr: number }) {
115
+ const { actual, ideal, adjusted, age, scr } = values;
116
+ qs<HTMLElement>(ctx.root, '#cgx-actual-weight').textContent = weightText(ctx, actual); qs<HTMLElement>(ctx.root, '#cgx-ibw').textContent = weightText(ctx, ideal); qs<HTMLElement>(ctx.root, '#cgx-abw').textContent = weightText(ctx, adjusted);
117
+ qs<HTMLElement>(ctx.root, '#cgx-actual-crcl').textContent = `${clearance(ctx.sex, age, actual, scr)} ${ctx.ui.clearanceUnit}`; qs<HTMLElement>(ctx.root, '#cgx-ideal-crcl').textContent = `${clearance(ctx.sex, age, ideal, scr)} ${ctx.ui.clearanceUnit}`;
118
+ qs<HTMLElement>(ctx.root, '#cgx-adjusted-crcl').textContent = `${clearance(ctx.sex, age, adjusted, scr)} ${ctx.ui.clearanceUnit}`;
119
+ }
120
+ function render(ctx: Ctx) {
121
+ const cm = heightCm(ctx), actual = actualKg(ctx), ideal = ibw(ctx.sex, cm), adjusted = round(ideal + 0.4 * (actual - ideal)), scr = Math.max(0.1, creatinineMgDl(ctx)), age = num(ctx.age);
122
+ const selectedWeight = calcWeight(ctx.weightMode, actual, ideal, adjusted), value = clearance(ctx.sex, age, selectedWeight, scr), bmi = round(actual / ((cm / 100) ** 2));
123
+ syncRanges(ctx); renderGauge(ctx, value, stage(value)); qs<HTMLElement>(ctx.root, '#cgx-bmi').textContent = String(bmi); qs<HTMLElement>(ctx.root, '#cgx-bmi-note').textContent = bmi > 30 ? ctx.ui.obesityNotice : ctx.ui.bmiNote;
124
+ renderWeights(ctx, { actual, ideal, adjusted, age, scr }); qs<HTMLElement>(ctx.root, '#cgx-calc-weight').textContent = weightText(ctx, selectedWeight); qs<HTMLElement>(ctx.root, '#cgx-selected-weight').textContent = ctx.ui[`${ctx.weightMode}WeightLabel`];
125
+ qs<HTMLElement>(ctx.root, '#cgx-formula-line').textContent = ctx.sex === 'female' ? ctx.ui.formulaFemale : ctx.ui.formulaMale; qs<HTMLElement>(ctx.root, '#cgx-creatinine-converted').textContent = ctx.creatinineUnit === 'mg/dL' ? `${round(mgToUmol(num(ctx.creatinine)))} ${ctx.ui.umolLUnit}` : `${round(scr)} ${ctx.ui.mgDlUnit}`;
126
+ qs<HTMLElement>(ctx.root, '#cgx-adjustment-note').textContent = bmi > 30 ? ctx.ui.obesityFlag : ctx.ui.noObesityFlag; save(ctx);
127
+ }
128
+ function createCtx(root: Element): Ctx {
129
+ return { root, ui: JSON.parse((root as HTMLElement).dataset.ui || '{}') as Record<string, string>, sex: 'male', weightMode: 'actual', unitSystem: 'metric', creatinineUnit: 'mg/dL', age: qs(root, '#cgx-age'), ageRange: qs(root, '#cgx-age-range'), weight: qs(root, '#cgx-weight'), weightRange: qs(root, '#cgx-weight-range'), heightCm: qs(root, '#cgx-height-cm'), heightFt: qs(root, '#cgx-height-ft'), heightIn: qs(root, '#cgx-height-in'), heightRange: qs(root, '#cgx-height-range'), weightUnitLabel: qs(root, '#cgx-weight-unit'), heightUnitLabel: qs(root, '#cgx-height-unit'), heightPair: qs(root, '#cgx-height-pair'), creatinine: qs(root, '#cgx-creatinine'), creatinineRange: qs(root, '#cgx-creatinine-range') };
130
+ }
131
+ function bindButtons(ctx: Ctx) {
132
+ ctx.root.querySelectorAll<HTMLButtonElement>('[data-sex]').forEach((button) => button.addEventListener('click', () => { ctx.sex = button.dataset.sex as Sex; press(ctx.root, '[data-sex]', button, 'is-on'); render(ctx); }));
133
+ ctx.root.querySelectorAll<HTMLButtonElement>('[data-weight-mode]').forEach((button) => button.addEventListener('click', () => { ctx.weightMode = button.dataset.weightMode as WeightMode; press(ctx.root, '[data-weight-mode]', button, 'is-picked'); render(ctx); }));
134
+ ctx.root.querySelectorAll<HTMLButtonElement>('[data-unit-system]').forEach((button) => button.addEventListener('click', () => { setUnitSystem(ctx, button.dataset.unitSystem as UnitSystem); press(ctx.root, '[data-unit-system]', button, 'is-on'); render(ctx); }));
135
+ ctx.root.querySelectorAll<HTMLButtonElement>('[data-creatinine-unit]').forEach((button) => button.addEventListener('click', () => { const next = button.dataset.creatinineUnit as CreatinineUnit; if (next !== ctx.creatinineUnit) ctx.creatinine.value = String(round(next === 'umol/L' ? mgToUmol(num(ctx.creatinine)) : umolToMg(num(ctx.creatinine)))); ctx.creatinineUnit = next; press(ctx.root, '[data-creatinine-unit]', button, 'is-on'); render(ctx); }));
136
+ }
137
+ function bindRanges(ctx: Ctx) {
138
+ ctx.ageRange.addEventListener('input', () => { ctx.age.value = ctx.ageRange.value; render(ctx); });
139
+ ctx.weightRange.addEventListener('input', () => { const kg = num(ctx.weightRange); ctx.weight.value = String(round(ctx.unitSystem === 'imperial' ? kgToLb(kg) : kg)); render(ctx); });
140
+ ctx.heightRange.addEventListener('input', () => { const cm = num(ctx.heightRange), inches = cm / 2.54; ctx.heightCm.value = String(round(cm)); ctx.heightFt.value = String(Math.floor(inches / 12)); ctx.heightIn.value = String(round(inches % 12)); render(ctx); });
141
+ ctx.creatinineRange.addEventListener('input', () => { const scr = num(ctx.creatinineRange); ctx.creatinine.value = String(round(ctx.creatinineUnit === 'umol/L' ? mgToUmol(scr) : scr)); render(ctx); });
142
+ }
143
+ function init(root: Element) {
144
+ const ctx = createCtx(root);
145
+ bindButtons(ctx); bindRanges(ctx);
146
+ [ctx.age, ctx.weight, ctx.heightCm, ctx.heightFt, ctx.heightIn, ctx.creatinine].forEach((input) => input.addEventListener('input', () => render(ctx)));
147
+ restore(ctx); render(ctx);
148
+ }
149
+ document.querySelectorAll('.cgx').forEach(init);
@@ -0,0 +1,147 @@
1
+ ---
2
+ import './creatinine-clearance-cockcroft-gault.css';
3
+ import type { CreatinineClearanceCockcroftGaultUI } from './ui';
4
+
5
+ interface Props {
6
+ locale?: string;
7
+ ui?: Record<string, unknown>;
8
+ }
9
+
10
+ const t = (Astro.props.ui || {}) as CreatinineClearanceCockcroftGaultUI;
11
+ ---
12
+
13
+ <div class="cgx" data-ui={JSON.stringify(t)}>
14
+ <section class="cgx__cockpit" aria-live="polite">
15
+ <div class="cgx__gauge" data-stage="mild" aria-label={t.clearanceLabel}>
16
+ <div class="cgx__halo">
17
+ <div class="cgx__value">
18
+ <span>{t.clearanceLabel}</span>
19
+ <strong id="cgx-clearance">78.2</strong>
20
+ <small>{t.clearanceUnit}</small>
21
+ </div>
22
+ </div>
23
+ <div class="cgx__stage-pill" id="cgx-stage-pill">{t.mildLabel}</div>
24
+ </div>
25
+
26
+ <div class="cgx__runway" aria-label={t.renalBarLabel}>
27
+ <div class="cgx__rail">
28
+ <span class="cgx__zone cgx__zone--failure">{t.failureLabel}<b>&lt;15</b></span>
29
+ <span class="cgx__zone cgx__zone--severe">{t.severeLabel}<b>15-29</b></span>
30
+ <span class="cgx__zone cgx__zone--moderate">{t.moderateLabel}<b>30-59</b></span>
31
+ <span class="cgx__zone cgx__zone--mild">{t.mildLabel}<b>60-89</b></span>
32
+ <span class="cgx__zone cgx__zone--normal">{t.normalLabel}<b>90-180+</b></span>
33
+ <i id="cgx-marker"></i>
34
+ </div>
35
+ <p id="cgx-stage-body">{t.mildBody}</p>
36
+ </div>
37
+ </section>
38
+
39
+ <section class="cgx__lab">
40
+ <div class="cgx__settings">
41
+ <div class="cgx__system" role="group" aria-label={t.unitSystemLabel}>
42
+ <button type="button" class="is-on" data-unit-system="metric">{t.metricLabel}</button>
43
+ <button type="button" data-unit-system="imperial">{t.imperialLabel}</button>
44
+ </div>
45
+ <div class="cgx__toggle" role="group" aria-label={t.sexLabel}>
46
+ <button type="button" class="is-on" data-sex="male">{t.maleLabel}</button>
47
+ <button type="button" data-sex="female">{t.femaleLabel}</button>
48
+ </div>
49
+ </div>
50
+
51
+ <div class="cgx__identity">
52
+ <div class="cgx__metric">
53
+ <span>{t.bmiLabel}</span>
54
+ <b id="cgx-bmi">26.8</b>
55
+ <small id="cgx-bmi-note">{t.bmiNote}</small>
56
+ </div>
57
+ </div>
58
+
59
+ <div class="cgx__inputs">
60
+ <label class="cgx__channel">
61
+ <span>{t.ageLabel}</span>
62
+ <input id="cgx-age-range" type="range" min="18" max="100" step="1" value="65" />
63
+ <div>
64
+ <input id="cgx-age" type="number" min="18" max="120" step="1" value="65" />
65
+ <small>{t.yearsUnit}</small>
66
+ </div>
67
+ </label>
68
+
69
+ <label class="cgx__channel">
70
+ <span>{t.weightLabel}</span>
71
+ <input id="cgx-weight-range" type="range" min="35" max="180" step="0.5" value="82" />
72
+ <div>
73
+ <input id="cgx-weight" type="number" min="1" step="0.1" value="82" />
74
+ <small id="cgx-weight-unit">{t.kgUnit}</small>
75
+ </div>
76
+ </label>
77
+
78
+ <label class="cgx__channel">
79
+ <span>{t.heightLabel}</span>
80
+ <input id="cgx-height-range" type="range" min="135" max="215" step="0.5" value="175" />
81
+ <div>
82
+ <input id="cgx-height-cm" type="number" min="80" step="0.1" value="175" />
83
+ <span class="cgx__height-pair" id="cgx-height-pair" hidden>
84
+ <input id="cgx-height-ft" type="number" min="2" step="1" value="5" aria-label={t.feetLabel} />
85
+ <input id="cgx-height-in" type="number" min="0" max="11.9" step="0.1" value="9" aria-label={t.inchesLabel} />
86
+ </span>
87
+ <small id="cgx-height-unit">{t.cmUnit}</small>
88
+ </div>
89
+ </label>
90
+
91
+ <label class="cgx__channel cgx__channel--creatinine">
92
+ <span>{t.creatinineLabel}</span>
93
+ <input id="cgx-creatinine-range" type="range" min="0.3" max="6" step="0.01" value="1.1" />
94
+ <div>
95
+ <input id="cgx-creatinine" type="number" min="0.1" step="0.01" value="1.1" />
96
+ <div class="cgx__mini-units" role="group" aria-label={t.creatinineLabel}>
97
+ <button type="button" class="is-on" data-creatinine-unit="mg/dL">{t.mgDlUnit}</button>
98
+ <button type="button" data-creatinine-unit="umol/L">{t.umolLUnit}</button>
99
+ </div>
100
+ </div>
101
+ </label>
102
+ </div>
103
+ </section>
104
+
105
+ <section class="cgx__weights" aria-label={t.weightModeLabel}>
106
+ <button type="button" class="cgx__weight-card is-picked" data-weight-mode="actual">
107
+ <span>{t.actualWeightLabel}</span>
108
+ <strong id="cgx-actual-weight">82.0 {t.kgUnit}</strong>
109
+ <em id="cgx-actual-crcl">78.2 {t.clearanceUnit}</em>
110
+ <small>{t.actualWeightHint}</small>
111
+ </button>
112
+ <button type="button" class="cgx__weight-card" data-weight-mode="ideal">
113
+ <span>{t.idealWeightLabel}</span>
114
+ <strong id="cgx-ibw">70.5 {t.kgUnit}</strong>
115
+ <em id="cgx-ideal-crcl">67.2 {t.clearanceUnit}</em>
116
+ <small>{t.idealWeightHint}</small>
117
+ </button>
118
+ <button type="button" class="cgx__weight-card" data-weight-mode="adjusted">
119
+ <span>{t.adjustedWeightLabel}</span>
120
+ <strong id="cgx-abw">75.1 {t.kgUnit}</strong>
121
+ <em id="cgx-adjusted-crcl">71.6 {t.clearanceUnit}</em>
122
+ <small>{t.adjustedWeightHint}</small>
123
+ </button>
124
+ </section>
125
+
126
+ <section class="cgx__readout">
127
+ <div class="cgx__formula">
128
+ <span>{t.formulaLabel}</span>
129
+ <code id="cgx-formula-line">{t.formulaMale}</code>
130
+ </div>
131
+ <div class="cgx__tiles">
132
+ <div><span>{t.calculationWeightLabel}</span><b id="cgx-calc-weight">82.0 {t.kgUnit}</b></div>
133
+ <div><span>{t.creatinineConvertedLabel}</span><b id="cgx-creatinine-converted">97.2 {t.umolLUnit}</b></div>
134
+ <div><span>{t.selectedWeightLabel}</span><b id="cgx-selected-weight">{t.actualWeightLabel}</b></div>
135
+ <div><span>{t.obesityCheckLabel}</span><b id="cgx-adjustment-note">{t.noObesityFlag}</b></div>
136
+ </div>
137
+ </section>
138
+
139
+ <section class="cgx__safety" role="alert">
140
+ <b>{t.warningTitle}</b>
141
+ <span>{t.warningBody}</span>
142
+ </section>
143
+ </div>
144
+
145
+ <script>
146
+ import './client';
147
+ </script>