feat: enhance CI workflow and update webapp styles
Some checks failed
CI / lint-and-test (push) Failing after 45s

- Added Node.js setup and webapp testing steps to the CI workflow for improved integration.
- Updated HTML to link multiple CSS files for better modularity and organization of styles.
- Removed deprecated `style.css` and introduced new CSS files for base styles, calendar, day detail, hints, markers, states, and duty list to enhance maintainability and readability.
- Implemented new styles for improved presentation of duty information and user interactions.
- Added unit tests for new API functions and contact link rendering to ensure functionality and reliability.
This commit is contained in:
2026-03-02 17:20:33 +03:00
parent e3240d0981
commit 2fb553567f
29 changed files with 2212 additions and 1375 deletions

View File

@@ -2,7 +2,7 @@
* Tooltips for calendar info buttons and duty markers.
*/
import { calendarEl, state } from "./dom.js";
import { getCalendarEl, state } from "./dom.js";
import { t } from "./i18n.js";
import { escapeHtml } from "./utils.js";
import { localDateString, formatHHMM } from "./dateUtils.js";
@@ -250,6 +250,7 @@ export function getDutyMarkerHintHtml(marker) {
* Remove active class from all duty/unavailable/vacation markers.
*/
export function clearActiveDutyMarker() {
const calendarEl = getCalendarEl();
if (!calendarEl) return;
calendarEl
.querySelectorAll(
@@ -261,6 +262,25 @@ export function clearActiveDutyMarker() {
/** Timeout for hiding duty marker hint on mouseleave (delegated). */
let dutyMarkerHideTimeout = null;
const HINT_FADE_MS = 150;
/**
* Dismiss a hint with fade-out: remove visible class, then after delay set hidden and remove data-active.
* @param {HTMLElement} hintEl - The hint element to dismiss
* @param {{ clearActive?: boolean, afterHide?: () => void }} opts - Optional: call clearActiveDutyMarker after hide; callback after hide
* @returns {number} Timeout id (for use with clearTimeout, e.g. when delegating hide to mouseout)
*/
export function dismissHint(hintEl, opts = {}) {
hintEl.classList.remove("calendar-event-hint--visible");
const id = setTimeout(() => {
hintEl.hidden = true;
hintEl.removeAttribute("data-active");
if (opts.clearActive) clearActiveDutyMarker();
if (typeof opts.afterHide === "function") opts.afterHide();
}, HINT_FADE_MS);
return id;
}
const DUTY_MARKER_SELECTOR = ".duty-marker, .unavailable-marker, .vacation-marker";
/**
@@ -304,6 +324,7 @@ function getOrCreateDutyMarkerHint() {
export function initHints() {
const calendarEventHint = getOrCreateCalendarEventHint();
const dutyMarkerHint = getOrCreateDutyMarkerHint();
const calendarEl = getCalendarEl();
if (!calendarEl) return;
calendarEl.addEventListener("click", (e) => {
@@ -317,11 +338,7 @@ export function initHints() {
positionHint(calendarEventHint, btn.getBoundingClientRect());
calendarEventHint.dataset.active = "1";
} else {
calendarEventHint.classList.remove("calendar-event-hint--visible");
setTimeout(() => {
calendarEventHint.hidden = true;
calendarEventHint.removeAttribute("data-active");
}, 150);
dismissHint(calendarEventHint);
}
return;
}
@@ -330,11 +347,7 @@ export function initHints() {
if (marker) {
e.stopPropagation();
if (marker.classList.contains("calendar-marker-active")) {
dutyMarkerHint.classList.remove("calendar-event-hint--visible");
setTimeout(() => {
dutyMarkerHint.hidden = true;
dutyMarkerHint.removeAttribute("data-active");
}, 150);
dismissHint(dutyMarkerHint);
marker.classList.remove("calendar-marker-active");
return;
}
@@ -377,31 +390,23 @@ export function initHints() {
const toMarker = e.relatedTarget instanceof HTMLElement ? e.relatedTarget.closest(DUTY_MARKER_SELECTOR) : null;
if (toMarker) return;
if (dutyMarkerHint.dataset.active) return;
dutyMarkerHint.classList.remove("calendar-event-hint--visible");
dutyMarkerHideTimeout = setTimeout(() => {
dutyMarkerHint.hidden = true;
dutyMarkerHideTimeout = null;
}, 150);
dutyMarkerHideTimeout = dismissHint(dutyMarkerHint, {
afterHide: () => {
dutyMarkerHideTimeout = null;
},
});
});
if (!state.calendarHintBound) {
state.calendarHintBound = true;
document.addEventListener("click", () => {
if (calendarEventHint.dataset.active) {
calendarEventHint.classList.remove("calendar-event-hint--visible");
setTimeout(() => {
calendarEventHint.hidden = true;
calendarEventHint.removeAttribute("data-active");
}, 150);
dismissHint(calendarEventHint);
}
});
document.addEventListener("keydown", (e) => {
if (e.key === "Escape" && calendarEventHint.dataset.active) {
calendarEventHint.classList.remove("calendar-event-hint--visible");
setTimeout(() => {
calendarEventHint.hidden = true;
calendarEventHint.removeAttribute("data-active");
}, 150);
dismissHint(calendarEventHint);
}
});
}
@@ -410,22 +415,12 @@ export function initHints() {
state.dutyMarkerHintBound = true;
document.addEventListener("click", () => {
if (dutyMarkerHint.dataset.active) {
dutyMarkerHint.classList.remove("calendar-event-hint--visible");
setTimeout(() => {
dutyMarkerHint.hidden = true;
dutyMarkerHint.removeAttribute("data-active");
clearActiveDutyMarker();
}, 150);
dismissHint(dutyMarkerHint, { clearActive: true });
}
});
document.addEventListener("keydown", (e) => {
if (e.key === "Escape" && dutyMarkerHint.dataset.active) {
dutyMarkerHint.classList.remove("calendar-event-hint--visible");
setTimeout(() => {
dutyMarkerHint.hidden = true;
dutyMarkerHint.removeAttribute("data-active");
clearActiveDutyMarker();
}, 150);
dismissHint(dutyMarkerHint, { clearActive: true });
}
});
}