mirror of
https://github.com/gauvainboiche/ketapk.git
synced 2026-09-02 11:13:11 +02:00
feat: Adding color themes + .ignore files update
This commit is contained in:
+131
-22
@@ -1,5 +1,5 @@
|
||||
import { MODES, PK_MODELS, CLINICAL_THRESHOLDS, APP_VERSION, generateSimulationData, processTimelineEvents } from './pkCore.js';
|
||||
import { adjustChartYScale } from './chartManager.js';
|
||||
import { adjustChartYScale, updateChartAnnotations } from './chartManager.js';
|
||||
import { initCalculatorUI } from './calculator.js';
|
||||
import { exportSimulationToCSV } from './csvExport.js';
|
||||
import { initI18n, getCurrentLang, t } from './i18n.js';
|
||||
@@ -8,6 +8,22 @@ let currentMode = MODES.ESKETAMINE;
|
||||
let pkChart = null;
|
||||
let currentSimulationData = null;
|
||||
|
||||
const THEME_STORAGE_KEY = 'keta_theme';
|
||||
const THEMES = ['dark', 'light', 'monokai'];
|
||||
|
||||
let timerMin = 5;
|
||||
let timespanCount = 60;
|
||||
|
||||
function buildTimelineInputs(interval, steps) {
|
||||
return Array.from({ length: steps + 1 }, (_, i) => ({
|
||||
time: i * interval,
|
||||
bolusMg: 0,
|
||||
perfRate: null
|
||||
}));
|
||||
}
|
||||
|
||||
let timelineInputs = buildTimelineInputs(timerMin, timespanCount);
|
||||
|
||||
function updateVersionDisplay() {
|
||||
const versionEl = document.querySelector('[data-i18n="app.version"]');
|
||||
if (versionEl) {
|
||||
@@ -15,16 +31,76 @@ function updateVersionDisplay() {
|
||||
}
|
||||
}
|
||||
|
||||
const timelineInputs = Array.from({ length: 61 }, (_, i) => ({
|
||||
time: i * 5,
|
||||
bolusMg: 0,
|
||||
perfRate: null
|
||||
}));
|
||||
function updateDynamicLabels() {
|
||||
const total = timerMin * timespanCount;
|
||||
|
||||
const titleEl = document.querySelector('[data-i18n="timeline.title"]');
|
||||
if (titleEl) titleEl.textContent = t('timeline.title', { total });
|
||||
|
||||
const summaryEl = document.querySelector('[data-i18n="table.summary_title"]');
|
||||
if (summaryEl) summaryEl.textContent = t('table.summary_title', { total });
|
||||
|
||||
const totalEl = document.getElementById('totalTimespanDisplay');
|
||||
if (totalEl) totalEl.textContent = t('patient.total_label', { total });
|
||||
}
|
||||
|
||||
function getStoredTheme() {
|
||||
const stored = localStorage.getItem(THEME_STORAGE_KEY);
|
||||
return THEMES.includes(stored) ? stored : 'dark';
|
||||
}
|
||||
|
||||
function applyTheme(theme) {
|
||||
const resolved = THEMES.includes(theme) ? theme : 'dark';
|
||||
document.documentElement.setAttribute('data-theme', resolved);
|
||||
localStorage.setItem(THEME_STORAGE_KEY, resolved);
|
||||
refreshChartTheme();
|
||||
}
|
||||
|
||||
function refreshChartTheme() {
|
||||
if (!pkChart) return;
|
||||
const styles = getComputedStyle(document.documentElement);
|
||||
const gridColor = styles.getPropertyValue('--kt-chart-grid').trim();
|
||||
const textColor = styles.getPropertyValue('--kt-chart-text').trim();
|
||||
const legendColor = styles.getPropertyValue('--kt-chart-legend').trim();
|
||||
|
||||
pkChart.options.scales.y.title.color = textColor;
|
||||
pkChart.options.scales.y.grid.color = gridColor;
|
||||
pkChart.options.scales.x.title.color = textColor;
|
||||
pkChart.options.scales.x.grid.color = gridColor;
|
||||
pkChart.options.plugins.legend.labels.color = legendColor;
|
||||
|
||||
// Les couleurs de texte des zones cliniques dépendent aussi du thème (cf. chartManager.js)
|
||||
updateChartAnnotations(pkChart, currentMode, pkChart.options.scales.y.max);
|
||||
pkChart.update();
|
||||
}
|
||||
|
||||
function readPositiveInt(el, fallback, max = Infinity) {
|
||||
const parsed = parseInt(el.value.trim(), 10);
|
||||
if (isNaN(parsed) || parsed < 1) return fallback;
|
||||
return Math.min(parsed, max);
|
||||
}
|
||||
|
||||
function enforceIntegerInput(el) {
|
||||
el?.addEventListener('input', () => {
|
||||
const cleaned = el.value.replace(/[^\d]/g, '');
|
||||
if (cleaned !== el.value) el.value = cleaned;
|
||||
});
|
||||
}
|
||||
|
||||
function rebuildTimeline() {
|
||||
timelineInputs = buildTimelineInputs(timerMin, timespanCount);
|
||||
pkChart.data.labels = timelineInputs.map(d => `${d.time}'`);
|
||||
renderTimelineGrid();
|
||||
updateDynamicLabels();
|
||||
updateSimulation();
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
applyTheme(getStoredTheme());
|
||||
|
||||
await initI18n();
|
||||
updateVersionDisplay();
|
||||
|
||||
|
||||
const selectLang = document.getElementById('selectLang');
|
||||
if (selectLang) {
|
||||
selectLang.value = getCurrentLang();
|
||||
@@ -32,12 +108,41 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||
await initI18n(e.target.value);
|
||||
updateVersionDisplay();
|
||||
renderTimelineGrid();
|
||||
updateDynamicLabels();
|
||||
updateSimulation();
|
||||
});
|
||||
}
|
||||
|
||||
const selectTheme = document.getElementById('selectTheme');
|
||||
if (selectTheme) {
|
||||
selectTheme.value = getStoredTheme();
|
||||
selectTheme.addEventListener('change', (e) => applyTheme(e.target.value));
|
||||
}
|
||||
|
||||
const timerInput = document.getElementById('timerInput');
|
||||
const timespanInput = document.getElementById('timespanInput');
|
||||
if (timerInput) {
|
||||
timerInput.value = timerMin;
|
||||
enforceIntegerInput(timerInput);
|
||||
timerInput.addEventListener('change', () => {
|
||||
timerMin = readPositiveInt(timerInput, timerMin);
|
||||
timerInput.value = timerMin;
|
||||
rebuildTimeline();
|
||||
});
|
||||
}
|
||||
if (timespanInput) {
|
||||
timespanInput.value = timespanCount;
|
||||
enforceIntegerInput(timespanInput);
|
||||
timespanInput.addEventListener('change', () => {
|
||||
timespanCount = readPositiveInt(timespanInput, timespanCount, 500);
|
||||
timespanInput.value = timespanCount;
|
||||
rebuildTimeline();
|
||||
});
|
||||
}
|
||||
|
||||
initChart();
|
||||
renderTimelineGrid();
|
||||
updateDynamicLabels();
|
||||
initCalculatorUI();
|
||||
setupEventListeners();
|
||||
updateSimulation();
|
||||
@@ -45,6 +150,10 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||
|
||||
function initChart() {
|
||||
const ctx = document.getElementById('pkChart').getContext('2d');
|
||||
const styles = getComputedStyle(document.documentElement);
|
||||
const gridColor = styles.getPropertyValue('--kt-chart-grid').trim();
|
||||
const textColor = styles.getPropertyValue('--kt-chart-text').trim();
|
||||
const legendColor = styles.getPropertyValue('--kt-chart-legend').trim();
|
||||
|
||||
pkChart = new Chart(ctx, {
|
||||
type: 'line',
|
||||
@@ -59,16 +168,16 @@ function initChart() {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
max: CLINICAL_THRESHOLDS[currentMode].maxScale,
|
||||
title: { display: true, text: 'Cp (ng/mL)', color: '#94a3b8' },
|
||||
grid: { color: 'rgba(51, 65, 85, 0.4)' }
|
||||
title: { display: true, text: 'Cp (ng/mL)', color: textColor },
|
||||
grid: { color: gridColor }
|
||||
},
|
||||
x: {
|
||||
title: { display: true, text: 'Temps (minutes)', color: '#94a3b8' },
|
||||
grid: { color: 'rgba(51, 65, 85, 0.4)' }
|
||||
title: { display: true, text: 'Temps (minutes)', color: textColor },
|
||||
grid: { color: gridColor }
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
legend: { labels: { color: '#f8fafc' } },
|
||||
legend: { labels: { color: legendColor } },
|
||||
annotation: { annotations: {} }
|
||||
}
|
||||
}
|
||||
@@ -156,7 +265,7 @@ function renderDataTable(simulationData) {
|
||||
|
||||
simulationData.timeline.forEach((item, index) => {
|
||||
const row = document.createElement('tr');
|
||||
row.className = index % 2 === 0 ? 'bg-slate-900/30' : 'bg-slate-800/30';
|
||||
row.className = index % 2 === 0 ? 'bg-[var(--kt-row-alt-1)]' : 'bg-[var(--kt-row-alt-2)]';
|
||||
|
||||
const dominoCp = (simulationData.domino && simulationData.domino[index]) ? Math.round(simulationData.domino[index].cp) : '-';
|
||||
const clementsCp = (simulationData.clements && simulationData.clements[index]) ? Math.round(simulationData.clements[index].cp) : '-';
|
||||
@@ -182,16 +291,16 @@ function renderTimelineGrid() {
|
||||
|
||||
timelineInputs.forEach((item, idx) => {
|
||||
const col = document.createElement('div');
|
||||
col.className = 'flex-1 bg-slate-900 border border-slate-700/60 rounded-xl p-2 text-center text-xs flex flex-col gap-1.5 min-w-[85px]';
|
||||
|
||||
col.className = 'flex-1 bg-[var(--kt-surface-3)] border border-[var(--kt-border-soft)] rounded-xl p-2 text-center text-xs flex flex-col gap-1.5 min-w-[85px]';
|
||||
|
||||
col.innerHTML = `
|
||||
<div class="font-bold text-amber-400 border-b border-slate-800 pb-1">${item.time} min</div>
|
||||
<div class="font-bold text-amber-400 border-b border-[var(--kt-border-soft)] pb-1">${item.time} min</div>
|
||||
<div>
|
||||
<span class="text-[10px] text-slate-400 block mb-0.5">${t('timeline.bolus')}</span>
|
||||
<input type="number" step="any" data-idx="${idx}" data-field="bolusMg" value="" placeholder="0" class="w-full bg-slate-800 border border-slate-700 text-center rounded text-white py-1 outline-none focus:border-amber-500">
|
||||
<span class="text-[10px] text-[var(--kt-text-3)] block mb-0.5">${t('timeline.bolus')}</span>
|
||||
<input type="number" step="any" data-idx="${idx}" data-field="bolusMg" value="" placeholder="0" class="w-full bg-[var(--kt-surface-2)] border border-[var(--kt-border)] text-center rounded text-[var(--kt-text-1)] py-1 outline-none focus:border-amber-500">
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-[10px] text-slate-400 block mb-0.5">${t('timeline.perf')}</span>
|
||||
<span class="text-[10px] text-[var(--kt-text-3)] block mb-0.5">${t('timeline.perf')}</span>
|
||||
<input type="number" step="any" data-idx="${idx}" data-field="perfRate" value="" placeholder="0" class="w-full border text-center rounded py-1 outline-none transition-colors">
|
||||
</div>
|
||||
`;
|
||||
@@ -223,7 +332,7 @@ function updateTimelineGridStyles() {
|
||||
input.value = timelineInputs[idx].perfRate !== null ? timelineInputs[idx].perfRate : '';
|
||||
}
|
||||
} else {
|
||||
input.className = 'w-full border text-center rounded py-1 outline-none transition-colors bg-slate-950/80 border-slate-800/50 text-slate-500 font-normal italic focus:border-amber-500';
|
||||
input.className = 'w-full border text-center rounded py-1 outline-none transition-colors bg-[var(--kt-surface-3)] border-[var(--kt-border-soft)] text-[var(--kt-text-3)] font-normal italic focus:border-amber-500';
|
||||
if (!isFocused) {
|
||||
input.value = event.infusionRate > 0 ? event.infusionRate : '';
|
||||
}
|
||||
@@ -309,10 +418,10 @@ function setMode(mode) {
|
||||
|
||||
if (mode === MODES.ESKETAMINE) {
|
||||
btnEsk.className = 'px-4 py-1.5 rounded-lg text-xs font-bold bg-amber-500 text-slate-950 shadow-md shadow-amber-500/30';
|
||||
btnRac.className = 'px-4 py-1.5 rounded-lg text-xs font-bold text-slate-400 hover:text-slate-200';
|
||||
btnRac.className = 'px-4 py-1.5 rounded-lg text-xs font-bold text-[var(--kt-text-3)] hover:text-[var(--kt-text-1)]';
|
||||
} else {
|
||||
btnRac.className = 'px-4 py-1.5 rounded-lg text-xs font-bold bg-fuchsia-500 text-slate-950 shadow-md shadow-fuchsia-500/30';
|
||||
btnEsk.className = 'px-4 py-1.5 rounded-lg text-xs font-bold text-slate-400 hover:text-slate-200';
|
||||
btnEsk.className = 'px-4 py-1.5 rounded-lg text-xs font-bold text-[var(--kt-text-3)] hover:text-[var(--kt-text-1)]';
|
||||
}
|
||||
updateSimulation();
|
||||
}
|
||||
Reference in New Issue
Block a user