From a78c45591fb3f06f4a6c9614c8094ac083080d39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20F=C3=B6rtsch?= Date: Fri, 6 Mar 2026 14:10:48 +0100 Subject: [PATCH] replace dead key panel with inline keyboard compositions clicking a dead key or pressing its key combo (e.g. Option+M) now shows compositions directly on the keyboard. keys with compositions are highlighted, others dimmed. spacebar shows catchy group name. click the active dead key again or press Escape to exit. add helper text beneath keyboard explaining viewer interaction. Co-Authored-By: Claude Opus 4.6 --- eurkey-macos.eu/index.html | 6 +- eurkey-macos.eu/keyboard.js | 219 +++++++++++++++++++++++++++--------- eurkey-macos.eu/style.css | 96 +++++++--------- 3 files changed, 210 insertions(+), 111 deletions(-) diff --git a/eurkey-macos.eu/index.html b/eurkey-macos.eu/index.html index dfc5007..30747e5 100644 --- a/eurkey-macos.eu/index.html +++ b/eurkey-macos.eu/index.html @@ -41,11 +41,7 @@
- +

Hold Shift or Option to preview modifier layers. Click a yellow dead key to see its compositions. Press Esc to go back.

diff --git a/eurkey-macos.eu/keyboard.js b/eurkey-macos.eu/keyboard.js index 8230cb6..7ffbe85 100644 --- a/eurkey-macos.eu/keyboard.js +++ b/eurkey-macos.eu/keyboard.js @@ -5,29 +5,29 @@ const KEYBOARD_ROWS = [ [10, 1.0, "§"], [18, 1.0, "1"], [19, 1.0, "2"], [20, 1.0, "3"], [21, 1.0, "4"], [23, 1.0, "5"], [22, 1.0, "6"], [26, 1.0, "7"], [28, 1.0, "8"], [25, 1.0, "9"], [29, 1.0, "0"], [27, 1.0, "-"], - [24, 1.0, "="], [null, 1.5, "⌫"], + [24, 1.0, "="], [null, 1.5, "\u232b"], ], [ - [null, 1.5, "⇥"], [12, 1.0, "Q"], [13, 1.0, "W"], [14, 1.0, "E"], + [null, 1.5, "\u21e5"], [12, 1.0, "Q"], [13, 1.0, "W"], [14, 1.0, "E"], [15, 1.0, "R"], [17, 1.0, "T"], [16, 1.0, "Y"], [32, 1.0, "U"], [34, 1.0, "I"], [31, 1.0, "O"], [35, 1.0, "P"], [33, 1.0, "["], [30, 1.0, "]"], ["spacer", 1.0, ""], ], [ - [null, 1.75, "⇪"], [0, 1.0, "A"], [1, 1.0, "S"], [2, 1.0, "D"], + [null, 1.75, "\u21ea"], [0, 1.0, "A"], [1, 1.0, "S"], [2, 1.0, "D"], [3, 1.0, "F"], [5, 1.0, "G"], [4, 1.0, "H"], [38, 1.0, "J"], [40, 1.0, "K"], [37, 1.0, "L"], [41, 1.0, ";"], [39, 1.0, "'"], - [42, 1.0, "\\"], ["enter", 0.75, "⏎"], + [42, 1.0, "\\"], ["enter", 0.75, "\u23ce"], ], [ - [null, 1.25, "⇧"], [50, 1.0, "`"], [6, 1.0, "Z"], [7, 1.0, "X"], + [null, 1.25, "\u21e7"], [50, 1.0, "`"], [6, 1.0, "Z"], [7, 1.0, "X"], [8, 1.0, "C"], [9, 1.0, "V"], [11, 1.0, "B"], [45, 1.0, "N"], [46, 1.0, "M"], [43, 1.0, ","], [47, 1.0, "."], [44, 1.0, "/"], - [null, 2.25, "⇧"], + [null, 2.25, "\u21e7"], ], [ - [null, 1.0, "fn"], [null, 1.0, "⌃"], [null, 1.0, "⌥"], [null, 1.25, "⌘"], - [null, 5.0, ""], [null, 1.25, "⌘"], [null, 1.0, "⌥"], + [null, 1.0, "fn"], [null, 1.0, "\u2303"], [null, 1.0, "\u2325"], [null, 1.25, "\u2318"], + ["spacebar", 5.0, ""], [null, 1.25, "\u2318"], [null, 1.0, "\u2325"], ["arrow-cluster", 3.0, ""], ], ]; @@ -46,9 +46,31 @@ const LAYERS = [ const MOD_LABELS = { "\u21e7": "shift", "\u2325": "option" }; +/* Browser keyCode → macOS key code mapping (US/ISO QWERTY) */ +const BROWSER_TO_MAC = { + 65: 0, 83: 1, 68: 2, 70: 3, 72: 4, 71: 5, 90: 6, 88: 7, + 67: 8, 86: 9, 192: 50, 66: 11, 81: 12, 87: 13, 69: 14, 82: 15, + 89: 16, 84: 17, 49: 18, 50: 19, 51: 20, 52: 21, 54: 22, 53: 23, + 187: 24, 57: 25, 55: 26, 189: 27, 56: 28, 48: 29, 221: 30, + 79: 31, 85: 32, 219: 33, 73: 34, 80: 35, 76: 37, 74: 38, + 222: 39, 75: 40, 186: 41, 220: 42, 188: 43, 191: 44, 78: 45, + 77: 46, 190: 47, 191: 44, 171: 24, 173: 27, 61: 24, 59: 41, +}; + +const DEAD_KEY_NAMES = { + "\u00b4": "The Acutes", "`": "The Graves", "^": "The Circumflexes", + "~": "The Tildes", "\u00a8": "The Umlauts", "\u02c7": "The H\u00e1\u010deks", + "\u00af": "The Macrons", "\u02da": "The Rings & Dots", + "\u03b1": "The Greeks", "\u221a": "The Mathematicians", + "\u00ac": "The Navigators", "\u00a9": "The Navigators", + " ": "The Mathematicians", +}; + const cache = new Map(); let currentVersion = "v2.0"; let currentData = null; +let currentDeadKey = null; +let keyElements = new Map(); async function loadVersion(version) { if (cache.has(version)) return cache.get(version); @@ -81,9 +103,27 @@ function clearElement(el) { while (el.firstChild) el.removeChild(el.firstChild); } +/* --- Build composition lookup for a dead key --- */ + +function buildCompositionMap(data, deadState) { + const dk = data.deadKeys[deadState]; + if (!dk || !dk.compositions) return null; + + const charMap = {}; + for (const [actionId, composed] of Object.entries(dk.compositions)) { + const action = data.actions[actionId]; + const base = action?.none || actionId; + if (base) charMap[base] = composed; + } + return charMap; +} + +/* --- Render keyboard --- */ + function renderKeyboard(data) { const kb = document.getElementById("keyboard"); clearElement(kb); + keyElements.clear(); for (const row of KEYBOARD_ROWS) { const rowEl = document.createElement("div"); @@ -113,15 +153,21 @@ function renderKeyboard(data) { spacerL.className = "arrow-key arrow-key--spacer"; const spacerR = document.createElement("div"); spacerR.className = "arrow-key arrow-key--spacer"; - topRow.append(spacerL, makeArrowKey("▲"), spacerR); + topRow.append(spacerL, makeArrowKey("\u25b2"), spacerR); const bottomRow = document.createElement("div"); bottomRow.className = "arrow-row"; - for (const sym of ["◀", "▼", "▶"]) { + for (const sym of ["\u25c0", "\u25bc", "\u25b6"]) { bottomRow.appendChild(makeArrowKey(sym)); } keyEl.append(topRow, bottomRow); + } else if (keyCode === "spacebar") { + keyEl.classList.add("key--mod", "key--spacebar"); + const span = document.createElement("span"); + span.className = "key-mod-label"; + span.textContent = label; + keyEl.appendChild(span); } else if (keyCode === "spacer") { keyEl.classList.add("key--spacer"); } else if (keyCode === "enter") { @@ -138,6 +184,8 @@ function renderKeyboard(data) { span.textContent = label; keyEl.appendChild(span); } else { + keyElements.set(keyCode, keyEl); + let hasDead = false; let deadState = null; @@ -159,7 +207,7 @@ function renderKeyboard(data) { if (hasDead) { keyEl.classList.add("key--dead"); keyEl.dataset.deadKey = deadState; - keyEl.addEventListener("click", () => showDeadKeyPanel(data, deadState)); + keyEl.addEventListener("click", () => toggleDeadKeyMode(deadState)); } } @@ -170,48 +218,92 @@ function renderKeyboard(data) { } } -function showDeadKeyPanel(data, state) { - const panel = document.getElementById("dead-key-panel"); - const title = document.getElementById("dead-key-title"); - const grid = document.getElementById("dead-key-grid"); +/* --- Dead key mode --- */ - const dk = data.deadKeys[state]; - if (!dk) return; +function toggleDeadKeyMode(deadState) { + if (currentDeadKey === deadState) { + exitDeadKeyMode(); + } else { + enterDeadKeyMode(deadState); + } +} - const terminator = dk.terminator || data.terminators[state] || ""; - title.textContent = state + " \u2192 " + displayChar(terminator); +function enterDeadKeyMode(deadState) { + if (!currentData) return; + const charMap = buildCompositionMap(currentData, deadState); + if (!charMap) return; - clearElement(grid); - const compositions = dk.compositions; - if (!compositions) return; + // clean up previous dead key mode if active + if (currentDeadKey) exitDeadKeyMode(); - for (const [actionId, composed] of Object.entries(compositions)) { - const action = data.actions[actionId]; - const base = action?.none || actionId; + currentDeadKey = deadState; + const kb = document.getElementById("keyboard"); + kb.classList.add("keyboard--dead-mode"); - const pair = document.createElement("div"); - pair.className = "dead-key-pair"; - - const baseSpan = document.createElement("span"); - baseSpan.className = "dead-key-base"; - baseSpan.textContent = displayChar(base); - - const arrow = document.createElement("span"); - arrow.className = "dead-key-arrow"; - arrow.textContent = "\u2192"; - - const composedSpan = document.createElement("span"); - composedSpan.className = "dead-key-composed"; - composedSpan.textContent = displayChar(composed); - - pair.appendChild(baseSpan); - pair.appendChild(arrow); - pair.appendChild(composedSpan); - grid.appendChild(pair); + // show catchy name on spacebar + const dk = currentData.deadKeys[deadState]; + const terminator = dk?.terminator || ""; + const catchy = DEAD_KEY_NAMES[terminator] || ""; + const spaceBar = kb.querySelector(".key--spacebar"); + if (spaceBar) { + spaceBar.querySelector(".key-mod-label").textContent = catchy; + spaceBar.classList.add("key--spacebar-label"); } - panel.hidden = false; - panel.scrollIntoView({ behavior: "smooth", block: "nearest" }); + for (const [keyCode, keyEl] of keyElements) { + const codeStr = String(keyCode); + const baseKey = currentData.keyMaps[MOD_BASE]?.keys[codeStr]; + const shiftKey = currentData.keyMaps[MOD_SHIFT]?.keys[codeStr]; + const baseChar = baseKey?.output || ""; + const shiftChar = shiftKey?.output || ""; + + const baseComposed = charMap[baseChar] || ""; + const shiftComposed = charMap[shiftChar] || ""; + + const spans = keyEl.querySelectorAll(".key-char"); + // order: shift, shift-option, base, option + if (spans[0]) spans[0].textContent = displayChar(shiftComposed); + if (spans[1]) spans[1].textContent = ""; + if (spans[2]) spans[2].textContent = displayChar(baseComposed); + if (spans[3]) spans[3].textContent = ""; + + if (keyEl.dataset.deadKey === deadState) { + keyEl.classList.add("key--dead-active"); + } else if (baseComposed || shiftComposed) { + keyEl.classList.add("key--has-composition"); + keyEl.classList.remove("key--no-composition"); + } else { + keyEl.classList.add("key--no-composition"); + keyEl.classList.remove("key--has-composition"); + } + } +} + +function exitDeadKeyMode() { + if (!currentDeadKey || !currentData) return; + currentDeadKey = null; + + const kb = document.getElementById("keyboard"); + kb.classList.remove("keyboard--dead-mode"); + + // restore spacebar + const spaceBar = kb.querySelector(".key--spacebar"); + if (spaceBar) { + spaceBar.querySelector(".key-mod-label").textContent = ""; + spaceBar.classList.remove("key--spacebar-label"); + } + + // restore original characters + for (const [keyCode, keyEl] of keyElements) { + keyEl.classList.remove("key--has-composition", "key--no-composition", "key--dead-active"); + + const spans = keyEl.querySelectorAll(".key-char"); + const layerOrder = [MOD_SHIFT, MOD_SHIFT_OPTION, MOD_BASE, MOD_OPTION]; + for (let i = 0; i < spans.length; i++) { + const info = charForKey(currentData, layerOrder[i], keyCode); + spans[i].textContent = info ? displayChar(info.char) : ""; + } + } } function showError(msg) { @@ -243,10 +335,39 @@ function updateActiveLayer() { } } +function getActiveModIndex() { + const shift = activeModifiers.has("shift"); + const option = activeModifiers.has("option"); + if (shift && option) return MOD_SHIFT_OPTION; + if (shift) return MOD_SHIFT; + if (option) return MOD_OPTION; + return MOD_BASE; +} + document.addEventListener("keydown", (e) => { if (e.key === "Shift") activeModifiers.add("shift"); if (e.key === "Alt") activeModifiers.add("option"); updateActiveLayer(); + + // detect dead key trigger from physical keyboard + if (currentData && !e.metaKey && !e.ctrlKey) { + const macCode = BROWSER_TO_MAC[e.keyCode]; + if (macCode !== undefined) { + const modIdx = getActiveModIndex(); + const keyMap = currentData.keyMaps[modIdx]; + const keyData = keyMap?.keys[String(macCode)]; + if (keyData?.deadKey) { + e.preventDefault(); + toggleDeadKeyMode(keyData.deadKey); + return; + } + } + } + + // Escape exits dead key mode + if (e.key === "Escape" && currentDeadKey) { + exitDeadKeyMode(); + } }); document.addEventListener("keyup", (e) => { @@ -279,8 +400,8 @@ function initTabs() { tabs.forEach(t => t.classList.remove("active")); tab.classList.add("active"); currentVersion = version; + currentDeadKey = null; - document.getElementById("dead-key-panel").hidden = true; updatePdfLink(); try { @@ -296,10 +417,6 @@ function initTabs() { /* --- Init --- */ -document.getElementById("dead-key-close").addEventListener("click", () => { - document.getElementById("dead-key-panel").hidden = true; -}); - initTabs(); loadVersion(currentVersion).then(data => { currentData = data; diff --git a/eurkey-macos.eu/style.css b/eurkey-macos.eu/style.css index 0ed559d..63b11f9 100644 --- a/eurkey-macos.eu/style.css +++ b/eurkey-macos.eu/style.css @@ -445,74 +445,60 @@ img { 0 0.5px 0 2px #464646; } -/* Dead key panel */ -.dead-key-panel { - max-width: fit-content; - margin: 1.5rem auto 0; - background: #fff; - border: 1px solid rgba(0, 0, 0, 0.1); - border-radius: 8px; - padding: 1.25rem; - position: relative; +/* Dead key mode — compositions shown on keyboard */ +.keyboard--dead-mode .key--no-composition { + opacity: 0.25; } -.dead-key-panel h4 { - font-size: 1rem; - font-weight: 600; - margin-bottom: 1rem; +.keyboard--dead-mode .key--has-composition { + background: linear-gradient(to bottom, #fef9e7, #fef3cd 95%, #f5e6a8); +} + +.keyboard--dead-mode .key--has-composition .key-char--shift-option, +.keyboard--dead-mode .key--has-composition .key-char--option { + visibility: hidden; +} + +.keyboard--dead-mode .key--has-composition .key-char--base { + font-size: 15px; + font-weight: 700; +} + +.keyboard--dead-mode .key--has-composition .key-char--shift { + font-size: 12px; + font-weight: 700; +} + +.keyboard--dead-mode .key--spacebar-label .key-mod-label { + font-size: 15px; + font-weight: 700; color: #1a1a2e; + letter-spacing: 0.02em; } -.dead-key-close { - position: absolute; - top: 0.75rem; - right: 0.75rem; - border: none; - background: none; - font-size: 1.25rem; +.keyboard--dead-mode .key--dead-active { + background: linear-gradient(to bottom, #d4e8ff, #b8d4f0 95%, #a0c0e0); cursor: pointer; + opacity: 1; +} + + +/* Keyboard hint */ +.keyboard-hint { + text-align: center; + font-size: 0.8rem; color: #5a6178; - padding: 0.25rem 0.5rem; - border-radius: 4px; + margin-top: 0.75rem; } -.dead-key-close:hover { - background: #f0f0f0; - color: #1a1a2e; -} - -.dead-key-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(80px, 1fr)); - gap: 0.5rem; -} - -.dead-key-pair { - display: flex; - align-items: center; - justify-content: center; - gap: 0.25rem; - background: #f5f7fa; - border-radius: 4px; - padding: 0.375rem 0.5rem; +.keyboard-hint kbd { + background: #e0e4ed; + border-radius: 3px; + padding: 0.1rem 0.35rem; font-family: "SF Mono", "Menlo", "Consolas", monospace; - font-size: 0.875rem; -} - -.dead-key-base { - color: #5a6178; -} - -.dead-key-arrow { - color: #aaa; font-size: 0.75rem; } -.dead-key-composed { - color: #1a1a2e; - font-weight: 600; -} - /* PDF links */ .layout-pdf-links { display: flex;