-
Layout Preview
+
EurKEY — The European Keyboard Layout
The complete key map for each version, showing all modifier layers and dead key compositions.
diff --git a/eurkey-macos.eu/keyboard.js b/eurkey-macos.eu/keyboard.js
index aed298d..8230cb6 100644
--- a/eurkey-macos.eu/keyboard.js
+++ b/eurkey-macos.eu/keyboard.js
@@ -262,6 +262,13 @@ window.addEventListener("blur", () => {
/* --- Version tabs --- */
+function updatePdfLink() {
+ const link = document.getElementById("pdf-download");
+ if (!link) return;
+ link.href = "pdf/eurkey-" + currentVersion + "-layout.pdf";
+ link.textContent = "Download " + currentVersion + " PDF";
+}
+
function initTabs() {
const tabs = document.querySelectorAll(".version-tab");
tabs.forEach(tab => {
@@ -274,6 +281,7 @@ function initTabs() {
currentVersion = version;
document.getElementById("dead-key-panel").hidden = true;
+ updatePdfLink();
try {
currentData = await loadVersion(version);
diff --git a/eurkey-macos.eu/style.css b/eurkey-macos.eu/style.css
index b5068fd..0ed559d 100644
--- a/eurkey-macos.eu/style.css
+++ b/eurkey-macos.eu/style.css
@@ -93,40 +93,19 @@ img {
text-decoration: none;
}
+.nav-links .btn--nav {
+ color: #fff;
+}
+
+.nav-links .btn--nav:hover {
+ color: #fff;
+}
+
.nav-github {
display: flex;
align-items: center;
}
-/* Hero */
-.hero {
- text-align: center;
- padding: 5rem 1.5rem 4rem;
- background: linear-gradient(180deg, #f0f4ff 0%, #fff 100%);
-}
-
-.hero-icon {
- border-radius: 20px;
- margin-bottom: 1.5rem;
-}
-
-.hero h1 {
- font-size: 2.5rem;
- font-weight: 800;
- letter-spacing: -0.02em;
- line-height: 1.15;
- margin-bottom: 1rem;
- max-width: 600px;
- margin-left: auto;
- margin-right: auto;
-}
-
-.hero-sub {
- font-size: 1.15rem;
- color: #5a6178;
- max-width: 480px;
- margin: 0 auto 2rem;
-}
.btn {
display: inline-block;
@@ -145,6 +124,24 @@ img {
text-decoration: none;
}
+.btn--nav {
+ padding: 0.4rem 1rem;
+ font-size: 0.85rem;
+ border-radius: 6px;
+ color: #fff;
+}
+
+.btn--secondary {
+ background: #fff;
+ color: #003399;
+ border: 1px solid #003399;
+}
+
+.btn--secondary:hover {
+ background: #f0f4ff;
+ color: #002266;
+}
+
/* Features */
.features {
padding: 4rem 0;
@@ -650,14 +647,6 @@ img {
}
@media (max-width: 768px) {
- .hero h1 {
- font-size: 1.75rem;
- }
-
- .hero-sub {
- font-size: 1rem;
- }
-
.feature-grid {
grid-template-columns: repeat(2, 1fr);
}
@@ -669,14 +658,6 @@ img {
display: none;
}
- .hero {
- padding: 3rem 1.5rem 2.5rem;
- }
-
- .hero h1 {
- font-size: 1.5rem;
- }
-
.feature-grid {
grid-template-columns: 1fr;
}
diff --git a/scripts/generate_layout_pdf.py b/scripts/generate_layout_pdf.py
index e2de9ac..216276c 100644
--- a/scripts/generate_layout_pdf.py
+++ b/scripts/generate_layout_pdf.py
@@ -36,34 +36,35 @@ 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, "="), (None, 2.0, "Backspace"),
+ (24, 1.0, "="), (None, 1.5, "⌫"),
],
- # Row 1: QWERTY row
+ # Row 1: QWERTY row (1.0u spacer at end for ISO enter)
[
- (None, 1.5, "Tab"), (12, 1.0, "Q"), (13, 1.0, "W"), (14, 1.0, "E"),
+ (None, 1.5, "⇥"), (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, "]"),
+ (30, 1.0, "]"), ("spacer", 1.0, ""),
],
- # Row 2: Home row
+ # Row 2: Home row (0.75u enter spanning into row 1)
[
- (None, 1.75, "Caps"), (0, 1.0, "A"), (1, 1.0, "S"), (2, 1.0, "D"),
+ (None, 1.75, "⇪"), (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, "\\"), (None, 1.75, "Enter"),
+ (42, 1.0, "\\"), ("enter", 0.75, "⏎"),
],
# Row 3: Bottom row
[
- (None, 1.25, "Shift"), (50, 1.0, "`"), (6, 1.0, "Z"), (7, 1.0, "X"),
+ (None, 1.25, "⇧"), (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, "/"),
- (None, 2.75, "Shift"),
+ (None, 2.25, "⇧"),
],
# Row 4: Modifier row
[
- (None, 1.5, "Ctrl"), (None, 1.25, "⌥"), (None, 1.5, "⌘"),
- (None, 6.0, ""), (None, 1.5, "⌘"), (None, 1.25, "⌥"),
- (None, 1.5, "Ctrl"),
+ (None, 1.0, "fn"), (None, 1.0, "⌃"), (None, 1.0, "⌥"),
+ (None, 1.25, "⌘"), (None, 5.0, ""),
+ (None, 1.25, "⌘"), (None, 1.0, "⌥"),
+ ("arrows", 3.0, ""),
],
]
@@ -176,7 +177,17 @@ class LayoutPDF(FPDF):
kw = width * KU - KEY_GAP
kh = KU - KEY_GAP
- if key_code is None or key_code not in TYPING_KEY_CODES:
+ if key_code == "spacer":
+ # invisible spacer — just advance x
+ pass
+ elif key_code == "enter":
+ # tall enter key spanning up into previous row
+ enter_h = 2 * KU - KEY_GAP
+ enter_y = y - KU
+ self._draw_mod_key(x, enter_y, kw, enter_h, label, key_code)
+ elif key_code == "arrows":
+ self._draw_arrow_cluster(x, y, kw, kh)
+ elif key_code is None or key_code not in TYPING_KEY_CODES:
self._draw_mod_key(x, y, kw, kh, label, key_code)
else:
self._draw_key(x, y, kw, kh, key_code, data)
@@ -213,12 +224,30 @@ class LayoutPDF(FPDF):
self.set_draw_color(*C_KEY_BORDER)
self.rect(x, y, w, h, "DF")
- # show physical label for modifier keys
- if label and key_code is None:
- self._font(8)
+ if label:
+ self._font(14, bold=True)
self._color((130, 130, 130))
- self.set_xy(x, y + h / 2 - 2)
- self.cell(w, 4, label, align="C")
+ self.set_xy(x, y + h / 2 - 3)
+ self.cell(w, 6, label, align="C")
+
+ def _draw_arrow_cluster(self, x, y, w, h):
+ """Draw inverted-T arrow keys."""
+ arrow_w = w / 3 - KEY_GAP * 0.67
+ arrow_h = h / 2 - KEY_GAP * 0.5
+ arrows_top = [("", arrow_w), ("▲", arrow_w), ("", arrow_w)]
+ arrows_bot = [("◀", arrow_w), ("▼", arrow_w), ("▶", arrow_w)]
+ for row_arrows, ay in [(arrows_top, y), (arrows_bot, y + h / 2)]:
+ ax = x
+ for label, aw in row_arrows:
+ if label:
+ self.set_fill_color(*C_MOD_BG)
+ self.set_draw_color(*C_KEY_BORDER)
+ self.rect(ax, ay, aw, arrow_h, "DF")
+ self._font(7)
+ self._color((130, 130, 130))
+ self.set_xy(ax, ay + arrow_h / 2 - 2)
+ self.cell(aw, 4, label, align="C")
+ ax += aw + KEY_GAP
def _draw_key(self, x, y, w, h, key_code, data):
"""Draw a typing key with 4 modifier layers."""
@@ -250,40 +279,67 @@ class LayoutPDF(FPDF):
self._color(color)
if pos == "bottom_left":
- self._font(12)
+ self._font(14)
self.set_xy(x + pad, mid_y - 0.5)
self.cell(w / 2 - pad, h / 2, char)
elif pos == "top_left":
- self._font(9)
+ self._font(11)
self.set_xy(x + pad, y + pad)
- self.cell(w / 2 - pad, 5, char)
+ self.cell(w / 2 - pad, 5.5, char)
elif pos == "bottom_right":
- self._font(9)
+ self._font(11)
self.set_xy(mid_x, mid_y - 0.5)
self.cell(w / 2 - pad, h / 2, char, align="R")
elif pos == "top_right":
- self._font(9)
+ self._font(11)
self.set_xy(mid_x, y + pad)
- self.cell(w / 2 - pad, 5, char, align="R")
+ self.cell(w / 2 - pad, 5.5, char, align="R")
+
+ # catchy names keyed by terminator character (stable across versions)
+ DEAD_KEY_NAMES_BY_TERMINATOR = {
+ "´": "The Acutes",
+ "`": "The Graves",
+ "^": "The Circumflexes",
+ "~": "The Tildes",
+ "¨": "The Umlauts",
+ "ˇ": "The Háčeks",
+ "¯": "The Macrons",
+ "˚": "The Rings & Dots",
+ "α": "The Greeks",
+ "√": "The Mathematicians",
+ "¬": "The Navigators",
+ "©": "The Navigators",
+ " ": "The Mathematicians",
+ }
+
+ def _find_dead_key_trigger(self, data, state_name):
+ """Find which key combo triggers a dead key state."""
+ mod_names = {
+ "0": "", "1": "⇧ ", "2": "⇪ ", "3": "⌥ ",
+ "4": "⇧⌥ ", "5": "⇪⌥ ",
+ }
+ # map key codes to physical labels from KEYBOARD_ROWS
+ code_labels = {}
+ for row in KEYBOARD_ROWS:
+ for key_code, _, label in row:
+ if isinstance(key_code, int):
+ code_labels[str(key_code)] = label
+ for mod_idx, km in data["keyMaps"].items():
+ for kc, kd in km["keys"].items():
+ if kd.get("deadKey") == state_name:
+ prefix = mod_names.get(mod_idx, f"mod{mod_idx} ")
+ key_label = code_labels.get(kc, f"key{kc}")
+ return f"{prefix}{key_label}"
+ return state_name
def _draw_dead_key_pages(self, data):
- """Draw dead key composition tables on new pages."""
+ """Draw dead key compositions as full keyboard layouts."""
dead_keys = data.get("deadKeys", {})
if not dead_keys:
return
actions = data.get("actions", {})
- self.add_page()
- self._font(18, bold=True)
- self._color((0, 0, 0))
- self.set_xy(MARGIN, 8)
- self.cell(0, 7, f"{self.layout_name} — Dead Key Compositions")
-
- y = 20
- col_w = 14 # width per composition entry
- max_cols = int((self.w - 2 * MARGIN) / col_w)
-
for state_name in sorted(dead_keys.keys()):
dk = dead_keys[state_name]
terminator = dk.get("terminator", "")
@@ -291,55 +347,96 @@ class LayoutPDF(FPDF):
if not compositions:
continue
- # build readable composition list: (base_char, composed_char)
- pairs = []
- for action_id, composed in sorted(compositions.items()):
- base = actions.get(action_id, {}).get("none", action_id)
- base = safe_char(base)
- composed = safe_char(composed)
- if base and composed:
- pairs.append((base, composed))
+ # build char → composed lookup
+ char_to_composed = {}
+ for action_id, composed in compositions.items():
+ base = actions.get(action_id, {}).get("none", "")
+ if base:
+ char_to_composed[base] = composed
- # estimate space needed
- num_rows = (len(pairs) + max_cols - 1) // max_cols
- needed = 10 + num_rows * 5.5
+ # skip if no compositions mapped
+ if not char_to_composed:
+ continue
- if y + needed > self.h - 10:
- self.add_page()
- y = 12
+ trigger = self._find_dead_key_trigger(data, state_name)
+ catchy = self.DEAD_KEY_NAMES_BY_TERMINATOR.get(terminator, state_name)
+ self.add_page()
- # header
- self._font(9, bold=True)
+ # title
+ self._font(18, bold=True)
self._color((0, 0, 0))
- self.set_xy(MARGIN, y)
- display = state_name
- self.cell(0, 5, f"{display} (terminator: {safe_char(terminator)})")
- y += 6
+ self.set_xy(MARGIN, 7)
+ self.cell(0, 7, f"{self.layout_name} — {catchy} ({trigger}, terminator: {safe_char(terminator)})")
- # composition grid
- col = 0
- for base, composed in pairs:
- cx = MARGIN + col * col_w
- self._font(9)
- self._color((100, 100, 100))
- self.set_xy(cx, y)
- self.cell(5, 5, base)
- self._color((0, 0, 0))
- self.set_xy(cx + 4, y)
- self.cell(9, 5, f"→{composed}")
+ # draw keyboard with compositions
+ kb_y = 24
+ for row_idx, row in enumerate(KEYBOARD_ROWS):
+ x = MARGIN
+ y = kb_y + row_idx * KU
+ for key_code, width, label in row:
+ kw = width * KU - KEY_GAP
+ kh = KU - KEY_GAP
- col += 1
- if col >= max_cols:
- col = 0
- y += 5.5
+ if key_code == "spacer":
+ pass
+ elif key_code == "enter":
+ enter_h = 2 * KU - KEY_GAP
+ enter_y = y - KU
+ self._draw_mod_key(x, enter_y, kw, enter_h, label, key_code)
+ elif key_code == "arrows":
+ self._draw_arrow_cluster(x, y, kw, kh)
+ elif not isinstance(key_code, int) or key_code not in TYPING_KEY_CODES:
+ self._draw_mod_key(x, y, kw, kh, label, key_code)
+ else:
+ self._draw_dead_composition_key(
+ x, y, kw, kh, key_code, data, char_to_composed,
+ )
- if col > 0:
- y += 4.5
- y += 3
+ x += width * KU
- if y > self.h - 15:
- self.add_page()
- y = 12
+ def _draw_dead_composition_key(self, x, y, w, h, key_code, data, char_to_composed):
+ """Draw a key showing dead key compositions for base and shift layers."""
+ code_str = str(key_code)
+ km_base = data["keyMaps"].get(MOD_BASE, {}).get("keys", {})
+ km_shift = data["keyMaps"].get(MOD_SHIFT, {}).get("keys", {})
+
+ base_char = km_base.get(code_str, {}).get("output", "")
+ shift_char = km_shift.get(code_str, {}).get("output", "")
+
+ base_composed = char_to_composed.get(base_char, "")
+ shift_composed = char_to_composed.get(shift_char, "")
+
+ # background — highlight if any composition exists
+ has_comp = bool(base_composed or shift_composed)
+ bg = C_DEAD_BG if has_comp else C_KEY_BG
+ self.set_fill_color(*bg)
+ self.set_draw_color(*C_KEY_BORDER)
+ self.rect(x, y, w, h, "DF")
+
+ pad = 1.2
+ mid_y = y + h / 2
+
+ if base_composed:
+ self._font(14)
+ self._color((0, 0, 0))
+ self.set_xy(x + pad, mid_y - 0.5)
+ self.cell(w / 2 - pad, h / 2, safe_char(base_composed))
+ elif base_char:
+ self._font(14)
+ self._color((200, 200, 200))
+ self.set_xy(x + pad, mid_y - 0.5)
+ self.cell(w / 2 - pad, h / 2, safe_char(base_char))
+
+ if shift_composed:
+ self._font(11)
+ self._color((0, 40, 170))
+ self.set_xy(x + pad, y + pad)
+ self.cell(w / 2 - pad, 5.5, safe_char(shift_composed))
+ elif shift_char:
+ self._font(11)
+ self._color((200, 200, 200))
+ self.set_xy(x + pad, y + pad)
+ self.cell(w / 2 - pad, 5.5, safe_char(shift_char))
def generate_pdf(version, output_dir):