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 <noreply@anthropic.com>
This commit is contained in:
2026-03-06 14:10:48 +01:00
parent f3c793c37f
commit a78c45591f
3 changed files with 210 additions and 111 deletions

View File

@@ -41,11 +41,7 @@
</div> </div>
</div> </div>
<figure class="keyboard" id="keyboard"></figure> <figure class="keyboard" id="keyboard"></figure>
<div class="dead-key-panel" id="dead-key-panel" hidden> <p class="keyboard-hint">Hold <kbd>Shift</kbd> or <kbd>Option</kbd> to preview modifier layers. Click a yellow dead key to see its compositions. Press <kbd>Esc</kbd> to go back.</p>
<button class="dead-key-close" id="dead-key-close" aria-label="Close">&times;</button>
<h4 id="dead-key-title"></h4>
<div class="dead-key-grid" id="dead-key-grid"></div>
</div>
<div class="layout-pdf-links"> <div class="layout-pdf-links">
<a href="pdf/eurkey-v2.0-layout.pdf" id="pdf-download" class="btn btn--secondary">Download v2.0 PDF</a> <a href="pdf/eurkey-v2.0-layout.pdf" id="pdf-download" class="btn btn--secondary">Download v2.0 PDF</a>
</div> </div>

View File

@@ -5,29 +5,29 @@ const KEYBOARD_ROWS = [
[10, 1.0, "§"], [18, 1.0, "1"], [19, 1.0, "2"], [20, 1.0, "3"], [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"], [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, "-"], [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"], [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, "["], [34, 1.0, "I"], [31, 1.0, "O"], [35, 1.0, "P"], [33, 1.0, "["],
[30, 1.0, "]"], ["spacer", 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"], [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, "'"], [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"], [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, "/"], [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, 1.0, "fn"], [null, 1.0, "\u2303"], [null, 1.0, "\u2325"], [null, 1.25, "\u2318"],
[null, 5.0, ""], [null, 1.25, ""], [null, 1.0, ""], ["spacebar", 5.0, ""], [null, 1.25, "\u2318"], [null, 1.0, "\u2325"],
["arrow-cluster", 3.0, ""], ["arrow-cluster", 3.0, ""],
], ],
]; ];
@@ -46,9 +46,31 @@ const LAYERS = [
const MOD_LABELS = { "\u21e7": "shift", "\u2325": "option" }; 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(); const cache = new Map();
let currentVersion = "v2.0"; let currentVersion = "v2.0";
let currentData = null; let currentData = null;
let currentDeadKey = null;
let keyElements = new Map();
async function loadVersion(version) { async function loadVersion(version) {
if (cache.has(version)) return cache.get(version); if (cache.has(version)) return cache.get(version);
@@ -81,9 +103,27 @@ function clearElement(el) {
while (el.firstChild) el.removeChild(el.firstChild); 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) { function renderKeyboard(data) {
const kb = document.getElementById("keyboard"); const kb = document.getElementById("keyboard");
clearElement(kb); clearElement(kb);
keyElements.clear();
for (const row of KEYBOARD_ROWS) { for (const row of KEYBOARD_ROWS) {
const rowEl = document.createElement("div"); const rowEl = document.createElement("div");
@@ -113,15 +153,21 @@ function renderKeyboard(data) {
spacerL.className = "arrow-key arrow-key--spacer"; spacerL.className = "arrow-key arrow-key--spacer";
const spacerR = document.createElement("div"); const spacerR = document.createElement("div");
spacerR.className = "arrow-key arrow-key--spacer"; spacerR.className = "arrow-key arrow-key--spacer";
topRow.append(spacerL, makeArrowKey(""), spacerR); topRow.append(spacerL, makeArrowKey("\u25b2"), spacerR);
const bottomRow = document.createElement("div"); const bottomRow = document.createElement("div");
bottomRow.className = "arrow-row"; bottomRow.className = "arrow-row";
for (const sym of ["", "", ""]) { for (const sym of ["\u25c0", "\u25bc", "\u25b6"]) {
bottomRow.appendChild(makeArrowKey(sym)); bottomRow.appendChild(makeArrowKey(sym));
} }
keyEl.append(topRow, bottomRow); 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") { } else if (keyCode === "spacer") {
keyEl.classList.add("key--spacer"); keyEl.classList.add("key--spacer");
} else if (keyCode === "enter") { } else if (keyCode === "enter") {
@@ -138,6 +184,8 @@ function renderKeyboard(data) {
span.textContent = label; span.textContent = label;
keyEl.appendChild(span); keyEl.appendChild(span);
} else { } else {
keyElements.set(keyCode, keyEl);
let hasDead = false; let hasDead = false;
let deadState = null; let deadState = null;
@@ -159,7 +207,7 @@ function renderKeyboard(data) {
if (hasDead) { if (hasDead) {
keyEl.classList.add("key--dead"); keyEl.classList.add("key--dead");
keyEl.dataset.deadKey = deadState; 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) { /* --- Dead key mode --- */
const panel = document.getElementById("dead-key-panel");
const title = document.getElementById("dead-key-title");
const grid = document.getElementById("dead-key-grid");
const dk = data.deadKeys[state]; function toggleDeadKeyMode(deadState) {
if (!dk) return; if (currentDeadKey === deadState) {
exitDeadKeyMode();
} else {
enterDeadKeyMode(deadState);
}
}
const terminator = dk.terminator || data.terminators[state] || ""; function enterDeadKeyMode(deadState) {
title.textContent = state + " \u2192 " + displayChar(terminator); if (!currentData) return;
const charMap = buildCompositionMap(currentData, deadState);
if (!charMap) return;
clearElement(grid); // clean up previous dead key mode if active
const compositions = dk.compositions; if (currentDeadKey) exitDeadKeyMode();
if (!compositions) return;
for (const [actionId, composed] of Object.entries(compositions)) { currentDeadKey = deadState;
const action = data.actions[actionId]; const kb = document.getElementById("keyboard");
const base = action?.none || actionId; kb.classList.add("keyboard--dead-mode");
const pair = document.createElement("div"); // show catchy name on spacebar
pair.className = "dead-key-pair"; const dk = currentData.deadKeys[deadState];
const terminator = dk?.terminator || "";
const baseSpan = document.createElement("span"); const catchy = DEAD_KEY_NAMES[terminator] || "";
baseSpan.className = "dead-key-base"; const spaceBar = kb.querySelector(".key--spacebar");
baseSpan.textContent = displayChar(base); if (spaceBar) {
spaceBar.querySelector(".key-mod-label").textContent = catchy;
const arrow = document.createElement("span"); spaceBar.classList.add("key--spacebar-label");
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);
} }
panel.hidden = false; for (const [keyCode, keyEl] of keyElements) {
panel.scrollIntoView({ behavior: "smooth", block: "nearest" }); 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) { 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) => { document.addEventListener("keydown", (e) => {
if (e.key === "Shift") activeModifiers.add("shift"); if (e.key === "Shift") activeModifiers.add("shift");
if (e.key === "Alt") activeModifiers.add("option"); if (e.key === "Alt") activeModifiers.add("option");
updateActiveLayer(); 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) => { document.addEventListener("keyup", (e) => {
@@ -279,8 +400,8 @@ function initTabs() {
tabs.forEach(t => t.classList.remove("active")); tabs.forEach(t => t.classList.remove("active"));
tab.classList.add("active"); tab.classList.add("active");
currentVersion = version; currentVersion = version;
currentDeadKey = null;
document.getElementById("dead-key-panel").hidden = true;
updatePdfLink(); updatePdfLink();
try { try {
@@ -296,10 +417,6 @@ function initTabs() {
/* --- Init --- */ /* --- Init --- */
document.getElementById("dead-key-close").addEventListener("click", () => {
document.getElementById("dead-key-panel").hidden = true;
});
initTabs(); initTabs();
loadVersion(currentVersion).then(data => { loadVersion(currentVersion).then(data => {
currentData = data; currentData = data;

View File

@@ -445,74 +445,60 @@ img {
0 0.5px 0 2px #464646; 0 0.5px 0 2px #464646;
} }
/* Dead key panel */ /* Dead key mode — compositions shown on keyboard */
.dead-key-panel { .keyboard--dead-mode .key--no-composition {
max-width: fit-content; opacity: 0.25;
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-panel h4 { .keyboard--dead-mode .key--has-composition {
font-size: 1rem; background: linear-gradient(to bottom, #fef9e7, #fef3cd 95%, #f5e6a8);
font-weight: 600; }
margin-bottom: 1rem;
.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; color: #1a1a2e;
letter-spacing: 0.02em;
} }
.dead-key-close { .keyboard--dead-mode .key--dead-active {
position: absolute; background: linear-gradient(to bottom, #d4e8ff, #b8d4f0 95%, #a0c0e0);
top: 0.75rem;
right: 0.75rem;
border: none;
background: none;
font-size: 1.25rem;
cursor: pointer; cursor: pointer;
opacity: 1;
}
/* Keyboard hint */
.keyboard-hint {
text-align: center;
font-size: 0.8rem;
color: #5a6178; color: #5a6178;
padding: 0.25rem 0.5rem; margin-top: 0.75rem;
border-radius: 4px;
} }
.dead-key-close:hover { .keyboard-hint kbd {
background: #f0f0f0; background: #e0e4ed;
color: #1a1a2e; border-radius: 3px;
} padding: 0.1rem 0.35rem;
.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;
font-family: "SF Mono", "Menlo", "Consolas", monospace; 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; font-size: 0.75rem;
} }
.dead-key-composed {
color: #1a1a2e;
font-weight: 600;
}
/* PDF links */ /* PDF links */
.layout-pdf-links { .layout-pdf-links {
display: flex; display: flex;