diff --git a/app.js b/app.js
index 129ab53..4f6cef7 100644
--- a/app.js
+++ b/app.js
@@ -196,27 +196,116 @@ class WortKraftApp {
}
setupHoverListeners() {
- document.addEventListener("mouseover", e => {
- if (!this.hoverTranslateEnabled) return;
- const tokenEl = e.target.closest(".word-token");
- if (!tokenEl) return;
- const word = tokenEl.dataset.word || tokenEl.textContent.trim();
- if (!word) return;
- this.showHoverTooltipForElement(tokenEl, word);
- });
+ let lastHoveredWord = null;
- document.addEventListener("mouseout", e => {
- const tokenEl = e.target.closest(".word-token");
- if (!tokenEl) return;
- this.hideHoverTooltip();
+ document.addEventListener("mousemove", e => {
+ if (!this.hoverTranslateEnabled) return;
+
+ // 1. Check if directly over an explicit .word-token span
+ const tokenEl = e.target.closest?.(".word-token");
+ if (tokenEl) {
+ const word = tokenEl.dataset.word || tokenEl.textContent.trim();
+ if (word) {
+ this.showHoverTooltipForRect(tokenEl.getBoundingClientRect(), word, true);
+ lastHoveredWord = word;
+ return;
+ }
+ }
+
+ // 2. Universal German word detector under cursor (works on headings, cards, buttons, chips, etc.)
+ const hit = this.getWordAtPoint(e.clientX, e.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;
+ }
+ }
+
+ if (lastHoveredWord) {
+ lastHoveredWord = null;
+ this.hideHoverTooltip();
+ }
});
window.addEventListener("scroll", () => this.hideHoverTooltip(), { passive: true });
}
- showHoverTooltipForElement(el, rawWord) {
- const tooltip = document.getElementById("hover-translate-tooltip");
- if (!tooltip) return;
+ getWordAtPoint(x, y) {
+ try {
+ let textNode = null;
+ let offset = 0;
+
+ if (document.caretRangeFromPoint) {
+ const range = document.caretRangeFromPoint(x, y);
+ if (!range) return null;
+ textNode = range.startContainer;
+ offset = range.startOffset;
+ } else if (document.caretPositionFromPoint) {
+ const pos = document.caretPositionFromPoint(x, y);
+ if (!pos) return null;
+ textNode = pos.offsetNode;
+ offset = pos.offset;
+ }
+
+ if (!textNode || textNode.nodeType !== Node.TEXT_NODE) return null;
+ const parentEl = textNode.parentElement;
+ if (!parentEl || parentEl.closest("#hover-translate-tooltip, input, textarea, select, script, style")) {
+ return null;
+ }
+
+ const text = textNode.textContent || "";
+ if (!text.trim()) return null;
+
+ const isGermanChar = ch => /[A-Za-zÄÖÜäöüß]/.test(ch);
+ let idx = offset;
+ if (idx >= text.length || !isGermanChar(text[idx])) {
+ if (idx > 0 && isGermanChar(text[idx - 1])) {
+ idx = idx - 1;
+ } else {
+ return null;
+ }
+ }
+
+ let start = idx;
+ let end = idx + 1;
+ while (start > 0 && isGermanChar(text[start - 1])) start--;
+ while (end < text.length && isGermanChar(text[end])) end++;
+
+ const word = text.slice(start, end);
+ if (word.length < 2) return null;
+
+ const wordRange = document.createRange();
+ wordRange.setStart(textNode, start);
+ wordRange.setEnd(textNode, end);
+ const rect = wordRange.getBoundingClientRect();
+
+ // Verify mouse pointer is actually inside/near the word's bounding box
+ if (
+ x < rect.left - 6 ||
+ x > rect.right + 6 ||
+ y < rect.top - 6 ||
+ y > rect.bottom + 6
+ ) {
+ return null;
+ }
+
+ return { word, rect };
+ } catch (_e) {
+ return null;
+ }
+ }
+
+ showHoverTooltipForRect(rect, rawWord, allowLiveFetch = true) {
+ let tooltip = document.getElementById("hover-translate-tooltip");
+ if (!tooltip) {
+ tooltip = document.createElement("div");
+ tooltip.id = "hover-translate-tooltip";
+ tooltip.className = "hover-translate-tooltip";
+ document.body.appendChild(tooltip);
+ }
const story = this.currentView === "reader"
? this.getAllStories().find(s => s.id === this.activeStoryId)
@@ -236,27 +325,43 @@ class WortKraftApp {
: "";
tooltip.innerHTML = `
-
+
${articleHtml}
${info.lemma || rawWord}
-
${info.english}
- ${info.plural ? `
Plural: ${info.plural}
` : ""}
+
${info.english}
+ ${info.plural ? `
Plural: ${info.plural}
` : ""}
`;
- const rect = el.getBoundingClientRect();
const left = Math.min(
window.innerWidth - 160,
Math.max(160, rect.left + rect.width / 2)
);
const top = Math.max(55, rect.top);
- tooltip.style.left = `${left}px`;
- tooltip.style.top = `${top}px`;
+ // Apply explicit inline critical styles in addition to CSS class so it is 100% immune to stale CSS cache
+ Object.assign(tooltip.style, {
+ position: "fixed",
+ zIndex: "9999",
+ background: "#18181b",
+ color: "#ffffff",
+ padding: "0.55rem 0.85rem",
+ borderRadius: "10px",
+ boxShadow: "0 10px 25px rgba(0,0,0,0.3)",
+ pointerEvents: "none",
+ left: `${left}px`,
+ top: `${top}px`,
+ transform: "translate(-50%, -100%) translateY(-8px)",
+ opacity: "1",
+ display: "block",
+ maxWidth: "310px",
+ fontSize: "0.84rem",
+ lineHeight: "1.35",
+ border: "1px solid rgba(255,255,255,0.16)"
+ });
tooltip.classList.add("visible");
- // If word is from a custom imported text and not in our local dictionary yet, fetch translation live
- if (info.needsLiveTranslate) {
+ if (allowLiveFetch && info.needsLiveTranslate) {
this.fetchLiveWordTranslation(rawWord);
}
}
@@ -284,7 +389,11 @@ class WortKraftApp {
hideHoverTooltip() {
const tooltip = document.getElementById("hover-translate-tooltip");
- if (tooltip) tooltip.classList.remove("visible");
+ if (tooltip) {
+ tooltip.classList.remove("visible");
+ tooltip.style.opacity = "0";
+ tooltip.style.display = "none";
+ }
}
// === AUDIO PRONUNCIATION (Server Native German Neural TTS + Browser Fallback) ===
diff --git a/data/hover_dict.js b/data/hover_dict.js
index 9b1b97b..d30b532 100644
--- a/data/hover_dict.js
+++ b/data/hover_dict.js
@@ -179,5 +179,62 @@ window.HOVER_DICT = {
"direkt": "directly / right", "café": "café (das Café)", "fall": "case (auf jeden Fall = definitely)", "bekommt": "gets / receives",
"per": "by / via", "mail": "email (die E-Mail)", "selbst": "even (selbst wenn = even if) / oneself", "so": "so / this way", "doch": "yet / however / after all",
"system": "system (das System)", "guten": "good (Guten Morgen/Tag)", "leid": "sorry (tut mir leid = I am sorry)", "klar": "clear / all right (Alles klar!)",
- "sonst": "otherwise / else", "hoffentlich": "hopefully", "hallo": "hello", "herzlich": "warmly / cordially", "willkommen": "welcome (Herzlich willkommen!)"
+ "sonst": "otherwise / else", "hoffentlich": "hopefully", "hallo": "hello", "herzlich": "warmly / cordially", "willkommen": "welcome (Herzlich willkommen!)",
+
+ // UI, Headings, Grammar Terms & Study Prompts
+ "wortkraft": "WortKraft (Word Power / Vocabulary Strength)",
+ "übersicht": "Overview / Dashboard (die Übersicht)",
+ "wortschatz": "Vocabulary (der Wortschatz — literally 'word-treasure')",
+ "trainer": "Trainer / Practice Coach (der Trainer)",
+ "leseecke": "Reading Corner (die Leseecke)",
+ "sprachpraxis": "Speaking & Writing Practice (die Sprachpraxis)",
+ "wörterbuch": "Dictionary / Vocabulary Bank (das Wörterbuch)",
+ "übersetzer": "Translator (der Übersetzer)",
+ "an": "ON / at / to", "aus": "OFF / out of / from",
+ "passiven": "passive", "aktiven": "active", "aktive": "active", "aktiver": "active",
+ "wiedererkennen": "recognition / recognizing again (das Wiedererkennen)",
+ "fällig": "due (for review)", "fällige": "due",
+ "wiederholung": "review / repetition (die Wiederholung)", "wiederholungen": "reviews / repetitions",
+ "bereit": "ready", "abruf": "recall / retrieval (der Abruf)",
+ "gemeistert": "mastered", "training": "training (das Training)",
+ "artikel": "article (der/die/das — der Artikel)",
+ "trefferquote": "accuracy / hit rate (die Trefferquote)",
+ "eingabe": "input / entry (die Eingabe)",
+ "sätze": "sentences (die Sätze)", "satz": "sentence (der Satz)",
+ "geschrieben": "written", "produziert": "produced",
+ "täglicher": "daily", "schritte": "steps (die Schritte)", "schritt": "step (der Schritt)",
+ "lückensatz": "cloze / fill-in-the-blank sentence (der Lückensatz)",
+ "lückensätze": "fill-in-the-blank sentences",
+ "ergänze": "complete / fill in (imperative)", "fehlende": "missing", "fehlendes": "missing",
+ "wort": "word (das Wort)", "worte": "words",
+ "wortart": "part of speech (die Wortart)",
+ "nomen": "noun (das Nomen)", "verben": "verbs (die Verben)", "ausdrücke": "expressions (die Ausdrücke)",
+ "präposition": "preposition (die Präposition)", "konnektoren": "connectors (die Konnektoren)",
+ "maskulin": "masculine (der)", "feminin": "feminine (die)", "neutrum": "neuter (das)", "plural": "plural (die)",
+ "karteikarte": "flashcard (die Karteikarte)", "wortbau": "word structure / morphology (der Wortbau)",
+ "eselsbrücke": "mnemonic / memory trick (die Eselsbrücke)",
+ "kollokationen": "collocations / word pairings", "formen": "forms",
+ "alltagstipp": "everyday usage tip (der Alltagstipp)",
+ "alltag": "everyday life (der Alltag)", "alltagssituation": "everyday situation", "alltagssituationen": "everyday situations",
+ "geschichte": "story (die Geschichte)", "geschichten": "stories (die Geschichten)",
+ "textauswahl": "text selection (die Textauswahl)", "eigener": "own / custom", "text": "text (der Text)",
+ "zielwörter": "target words (die Zielwörter)", "zielwörtern": "target words (Dative)",
+ "fokus": "focus (der Fokus)", "tages": "of the day (des Tages)",
+ "mischen": "to shuffle / mix", "jetzt": "now", "trainieren": "to train / practice",
+ "prüfen": "to check / verify", "lösung": "solution / answer (die Lösung)", "zeigen": "to show",
+ "nochmal": "again / once more", "leicht": "easy / light",
+ "umdrehen": "to flip / turn over", "vorlesen": "to read aloud", "aussprache": "pronunciation (die Aussprache)",
+ "übersetzung": "translation (die Übersetzung)", "ausblenden": "to hide", "anzeigen": "to show / display",
+ "szenarien": "scenarios (die Szenarien)", "rollenspiele": "roleplays (die Rollenspiele)", "rollenspiel": "roleplay (das Rollenspiel)",
+ "aufgabe": "task / prompt (die Aufgabe)", "senden": "to send",
+ "starthilfe": "starter hint / jump-start (die Starthilfe)", "mustersatz": "sample sentence (der Mustersatz)", "einfügen": "to insert",
+ "dialog": "dialogue (der Dialog)", "starten": "to start", "üben": "to practice",
+ "archiv": "archive / bank (das Archiv)", "kategorien": "categories",
+ "bedeutung": "meaning (die Bedeutung)", "beispielsatz": "example sentence (der Beispielsatz)",
+ "speichern": "to save", "gespeichert": "saved", "abbrechen": "to cancel", "hinzufügen": "to add",
+ "einstellungen": "settings (die Einstellungen)", "fortschritt": "progress (der Fortschritt)",
+ "exportieren": "to export", "lernstand": "learning progress", "zurücksetzen": "to reset",
+ "verständnis": "understanding / comprehension (das Verständnis)",
+ "klicke": "click (imperative)", "wähle": "choose / select (imperative)", "versuche": "try (imperative)",
+ "fahre": "move / hover / drive", "maus": "mouse (die Maus)", "beliebiges": "any", "stammform": "base form / lemma"
};
diff --git a/index.html b/index.html
index eaeabb2..f60ef4a 100644
--- a/index.html
+++ b/index.html
@@ -7,7 +7,7 @@
-
+
@@ -408,10 +408,10 @@
-
-
-
-
-
+
+
+
+
+