diff --git a/eurkey-macos.eu/index.html b/eurkey-macos.eu/index.html index 1eec71b..dfc5007 100644 --- a/eurkey-macos.eu/index.html +++ b/eurkey-macos.eu/index.html @@ -17,9 +17,7 @@ EurKEY - -
- EurKEY icon -

The European Keyboard Layout for macOS

-

Dead keys for diacritics. ISO international layout. Designed for MacBook keyboards.

-
Download on GitHub -
-
-

Layout Preview

+

EurKEY — The European Keyboard Layout

The complete key map for each version, showing all modifier layers and dead key compositions.

@@ -57,10 +47,7 @@
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):