Improve mobile UI responsiveness, touch tap-to-translate, and PWA icon

This commit is contained in:
chrisf committed 2026-10-21 04:58:22 +00:00
1 parent 5f013dbdfe
commit c3e66b4c98
6 files changed
+442 -28

No files matched your search

+46 -16
View File
@@ -218,38 +218,65 @@ class WortKraftApp {
setupHoverListeners() {
let lastHoveredWord = null;
document.addEventListener("mousemove", e => {
if (!this.hoverTranslateEnabled) return;
const handlePointLookup = (clientX, clientY, targetEl) => {
if (!this.hoverTranslateEnabled) return false;
// 1. Check if directly over an explicit .word-token span
const tokenEl = e.target.closest?.(".word-token");
// 1. Check if directly over/tapping an explicit .word-token span
const tokenEl = targetEl?.closest?.(".word-token");
if (tokenEl) {
const word = tokenEl.dataset.word || tokenEl.textContent.trim();
if (word) {
this.showHoverTooltipForRect(tokenEl.getBoundingClientRect(), word, true);
lastHoveredWord = word;
return;
return true;
}
}
// 2. Universal German word detector under cursor (works on headings, cards, buttons, chips, etc.)
const hit = this.getWordAtPoint(e.clientX, e.clientY);
// 2. Universal German word detector under cursor/tap
const hit = this.getWordAtPoint(clientX, clientY);
if (hit && hit.word) {
const info = this.lookupWordInfo(hit.word, null);
// Show tooltip if word is in our German dictionaries
if (!info.needsLiveTranslate) {
this.showHoverTooltipForRect(hit.rect, hit.word, false);
lastHoveredWord = hit.word;
return;
return true;
}
}
return false;
};
if (lastHoveredWord) {
document.addEventListener("mousemove", e => {
if (!this.hoverTranslateEnabled) return;
const matched = handlePointLookup(e.clientX, e.clientY, e.target);
if (!matched && lastHoveredWord) {
lastHoveredWord = null;
this.hideHoverTooltip();
}
});
// Mobile & Touch support: tap a word to show translation tooltip, tap elsewhere to dismiss
document.addEventListener(
"touchstart",
e => {
if (!this.hoverTranslateEnabled) return;
const touch = e.touches?.[0];
if (!touch) return;
// Ignore taps on interactive controls like buttons/inputs unless it's a .word-token
const isToken = e.target?.closest?.(".word-token");
const isInteractiveControl = e.target?.closest?.("button, input, select, textarea, a");
if (isInteractiveControl && !isToken) {
this.hideHoverTooltip();
return;
}
const matched = handlePointLookup(touch.clientX, touch.clientY, e.target);
if (!matched) {
lastHoveredWord = null;
this.hideHoverTooltip();
}
},
{ passive: true }
);
window.addEventListener("scroll", () => this.hideHoverTooltip(), { passive: true });
}
@@ -353,11 +380,14 @@ class WortKraftApp {
${info.plural ? `<div class="hover-tt-note" style="color:#a1a1aa;font-size:0.74rem;margin-top:0.15rem;">Plural: ${info.plural}</div>` : ""}
`;
const vw = window.innerWidth || 360;
const maxW = Math.min(300, vw - 24);
const halfW = Math.round(maxW / 2) + 12;
const left = Math.min(
window.innerWidth - 160,
Math.max(160, rect.left + rect.width / 2)
Math.max(halfW, vw - halfW),
Math.max(halfW, rect.left + rect.width / 2)
);
const top = Math.max(55, rect.top);
const top = Math.max(64, rect.top);
// Apply explicit inline critical styles in addition to CSS class so it is 100% immune to stale CSS cache
Object.assign(tooltip.style, {
@@ -374,7 +404,7 @@ class WortKraftApp {
transform: "translate(-50%, -100%) translateY(-8px)",
opacity: "1",
display: "block",
maxWidth: "310px",
maxWidth: `${maxW}px`,
fontSize: "0.84rem",
lineHeight: "1.35",
border: "1px solid rgba(255,255,255,0.16)"
@@ -2368,7 +2398,7 @@ class WortKraftApp {
</div>
</div>
<div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 0.75rem; margin: 1.25rem 0;">
<div class="prep-options-grid">
${optionsHtml}
</div>
@@ -2416,7 +2446,7 @@ class WortKraftApp {
<div class="cloze-en" style="margin-top: 0.3rem;">Gesuchtes Verb: <strong>${d.verb} (${d.english})</strong></div>
</div>
<div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 0.65rem; margin: 1.25rem 0;">
<div class="dawo-options-grid">
${daChoices
.map(
ch => `