@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.
- package/package.json +1 -1
- package/src/category/index.ts +2 -1
- package/src/entries.ts +3 -1
- package/src/index.ts +1 -0
- package/src/tests/locale_completeness.test.ts +1 -1
- package/src/tests/tool_validation.test.ts +2 -2
- package/src/tool/creatinineClearanceCockcroftGault/bibliography.astro +14 -0
- package/src/tool/creatinineClearanceCockcroftGault/bibliography.ts +28 -0
- package/src/tool/creatinineClearanceCockcroftGault/client.ts +149 -0
- package/src/tool/creatinineClearanceCockcroftGault/component.astro +147 -0
- package/src/tool/creatinineClearanceCockcroftGault/creatinine-clearance-cockcroft-gault.css +550 -0
- package/src/tool/creatinineClearanceCockcroftGault/entry.ts +27 -0
- package/src/tool/creatinineClearanceCockcroftGault/i18n/de.ts +306 -0
- package/src/tool/creatinineClearanceCockcroftGault/i18n/en.ts +306 -0
- package/src/tool/creatinineClearanceCockcroftGault/i18n/es.ts +306 -0
- package/src/tool/creatinineClearanceCockcroftGault/i18n/fr.ts +306 -0
- package/src/tool/creatinineClearanceCockcroftGault/i18n/id.ts +306 -0
- package/src/tool/creatinineClearanceCockcroftGault/i18n/it.ts +306 -0
- package/src/tool/creatinineClearanceCockcroftGault/i18n/ja.ts +306 -0
- package/src/tool/creatinineClearanceCockcroftGault/i18n/ko.ts +306 -0
- package/src/tool/creatinineClearanceCockcroftGault/i18n/nl.ts +306 -0
- package/src/tool/creatinineClearanceCockcroftGault/i18n/pl.ts +306 -0
- package/src/tool/creatinineClearanceCockcroftGault/i18n/pt.ts +306 -0
- package/src/tool/creatinineClearanceCockcroftGault/i18n/ru.ts +306 -0
- package/src/tool/creatinineClearanceCockcroftGault/i18n/sv.ts +306 -0
- package/src/tool/creatinineClearanceCockcroftGault/i18n/tr.ts +306 -0
- package/src/tool/creatinineClearanceCockcroftGault/i18n/zh.ts +306 -0
- package/src/tool/creatinineClearanceCockcroftGault/index.ts +12 -0
- package/src/tool/creatinineClearanceCockcroftGault/logic.ts +99 -0
- package/src/tool/creatinineClearanceCockcroftGault/seo.astro +15 -0
- package/src/tool/creatinineClearanceCockcroftGault/ui.ts +59 -0
- package/src/tools.ts +2 -1
package/package.json
CHANGED
package/src/category/index.ts
CHANGED
|
@@ -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);
|
|
@@ -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 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><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>
|