From cf2b90809a95379f9badb06b8811ad2a0d15e1f5 Mon Sep 17 00:00:00 2001 From: Leo Vasanko Date: Tue, 26 May 2026 16:37:12 +0000 Subject: [PATCH] HexKeyboard: refactor to row-based flex layout Replace 39 per-key absolute positioning CSS rules with 4 row divs using flexbox. Rows overlap via negative margin-top to preserve honeycomb staggering. Horizontal offsets via 4 margin-left rules. - Add RowKeyDef with globalIndex for row-based rendering - Add rows computed to group layoutKeys into 4 arrays - Remove all .hex-key-N { top; left } positioning rules - Keys are now flex children, position: relative, fixed width Also includes prior changes: - Row-based greyscale gradients (lighter top, darker home row) - Per-key re-triggerable press animation (DOM reflow reset) - Green-to-white exponential fade on keypress - Gamepad repeat: 90ms horizontal/buttons, 140ms vertical --- frontend/src/components/HexKeyboard.vue | 104 +++++++++++------------- 1 file changed, 46 insertions(+), 58 deletions(-) diff --git a/frontend/src/components/HexKeyboard.vue b/frontend/src/components/HexKeyboard.vue index e9582c2..86fe7a5 100644 --- a/frontend/src/components/HexKeyboard.vue +++ b/frontend/src/components/HexKeyboard.vue @@ -3,18 +3,24 @@
- +
(() => { + const result: RowKeyDef[][] = [[], [], [], []] + for (let i = 0; i < layoutKeys.length; i++) { + const key = layoutKeys[i] + const coord = getCoord(i) + result[coord.row].push({ ...key, globalIndex: i }) + } + return result +}) + // Four-row layout with honeycomb staggering // Each row shifted 0.5 cell left relative to the one below: // Row 0 (numbers): shift 0 @@ -470,16 +490,32 @@ onUnmounted(() => { .hex-keyboard-grid { position: relative; + display: flex; + flex-direction: column; + align-items: flex-start; --key-h: 56px; --key-w: calc(0.866 * var(--key-h)); - width: calc(var(--key-w) * 12.5); - height: calc(var(--key-h) * 3.25); } +.hex-keyboard-row { + display: flex; +} + +/* Overlap: each row overlaps the previous by 0.25 * key-h */ +.hex-keyboard-row:not(:first-child) { + margin-top: calc(var(--key-h) * -0.25); +} + +/* Row horizontal offsets for honeycomb staggering */ +.hex-keyboard-row-0 { margin-left: 0; } +.hex-keyboard-row-1 { margin-left: calc(var(--key-w) * 0.5); } +.hex-keyboard-row-2 { margin-left: calc(var(--key-w) * 1.0); } +.hex-keyboard-row-3 { margin-left: calc(var(--key-w) * 1.5); } + .hex-key { - position: absolute; + position: relative; height: var(--key-h); - aspect-ratio: 0.866 / 1; + width: var(--key-w); border: none; color: #ffffff; font-size: 0.95rem; @@ -565,54 +601,6 @@ onUnmounted(() => { 50% { opacity: 0.4; } } -/* Row positioning */ -/* Row 0 (numbers + BS) — shift 0 */ -.hex-key-0 { top: calc(var(--key-h) * 0.00); left: calc(var(--key-w) * 0.0); } -.hex-key-1 { top: calc(var(--key-h) * 0.00); left: calc(var(--key-w) * 1.0); } -.hex-key-2 { top: calc(var(--key-h) * 0.00); left: calc(var(--key-w) * 2.0); } -.hex-key-3 { top: calc(var(--key-h) * 0.00); left: calc(var(--key-w) * 3.0); } -.hex-key-4 { top: calc(var(--key-h) * 0.00); left: calc(var(--key-w) * 4.0); } -.hex-key-5 { top: calc(var(--key-h) * 0.00); left: calc(var(--key-w) * 5.0); } -.hex-key-6 { top: calc(var(--key-h) * 0.00); left: calc(var(--key-w) * 6.0); } -.hex-key-7 { top: calc(var(--key-h) * 0.00); left: calc(var(--key-w) * 7.0); } -.hex-key-8 { top: calc(var(--key-h) * 0.00); left: calc(var(--key-w) * 8.0); } -.hex-key-9 { top: calc(var(--key-h) * 0.00); left: calc(var(--key-w) * 9.0); } -.hex-key-10 { top: calc(var(--key-h) * 0.00); left: calc(var(--key-w) * 10.0); } - -/* Row 1 (QWERTY) — shift 0.5 */ -.hex-key-11 { top: calc(var(--key-h) * 0.75); left: calc(var(--key-w) * 0.5); } -.hex-key-12 { top: calc(var(--key-h) * 0.75); left: calc(var(--key-w) * 1.5); } -.hex-key-13 { top: calc(var(--key-h) * 0.75); left: calc(var(--key-w) * 2.5); } -.hex-key-14 { top: calc(var(--key-h) * 0.75); left: calc(var(--key-w) * 3.5); } -.hex-key-15 { top: calc(var(--key-h) * 0.75); left: calc(var(--key-w) * 4.5); } -.hex-key-16 { top: calc(var(--key-h) * 0.75); left: calc(var(--key-w) * 5.5); } -.hex-key-17 { top: calc(var(--key-h) * 0.75); left: calc(var(--key-w) * 6.5); } -.hex-key-18 { top: calc(var(--key-h) * 0.75); left: calc(var(--key-w) * 7.5); } -.hex-key-19 { top: calc(var(--key-h) * 0.75); left: calc(var(--key-w) * 8.5); } -.hex-key-20 { top: calc(var(--key-h) * 0.75); left: calc(var(--key-w) * 9.5); } - -/* Row 2 (ASDF) — shift 1.0 */ -.hex-key-21 { top: calc(var(--key-h) * 1.50); left: calc(var(--key-w) * 1.0); } -.hex-key-22 { top: calc(var(--key-h) * 1.50); left: calc(var(--key-w) * 2.0); } -.hex-key-23 { top: calc(var(--key-h) * 1.50); left: calc(var(--key-w) * 3.0); } -.hex-key-24 { top: calc(var(--key-h) * 1.50); left: calc(var(--key-w) * 4.0); } -.hex-key-25 { top: calc(var(--key-h) * 1.50); left: calc(var(--key-w) * 5.0); } -.hex-key-26 { top: calc(var(--key-h) * 1.50); left: calc(var(--key-w) * 6.0); } -.hex-key-27 { top: calc(var(--key-h) * 1.50); left: calc(var(--key-w) * 7.0); } -.hex-key-28 { top: calc(var(--key-h) * 1.50); left: calc(var(--key-w) * 8.0); } -.hex-key-29 { top: calc(var(--key-h) * 1.50); left: calc(var(--key-w) * 9.0); } - -/* Row 3 (ZXCV + Space + Close) — shift 1.5 */ -.hex-key-30 { top: calc(var(--key-h) * 2.25); left: calc(var(--key-w) * 1.5); } -.hex-key-31 { top: calc(var(--key-h) * 2.25); left: calc(var(--key-w) * 2.5); } -.hex-key-32 { top: calc(var(--key-h) * 2.25); left: calc(var(--key-w) * 3.5); } -.hex-key-33 { top: calc(var(--key-h) * 2.25); left: calc(var(--key-w) * 4.5); } -.hex-key-34 { top: calc(var(--key-h) * 2.25); left: calc(var(--key-w) * 5.5); } -.hex-key-35 { top: calc(var(--key-h) * 2.25); left: calc(var(--key-w) * 6.5); } -.hex-key-36 { top: calc(var(--key-h) * 2.25); left: calc(var(--key-w) * 7.5); } -.hex-key-37 { top: calc(var(--key-h) * 2.25); left: calc(var(--key-w) * 8.5); } -.hex-key-38 { top: calc(var(--key-h) * 2.25); left: calc(var(--key-w) * 9.5); } - /* Transition */ .hex-keyboard-fade-enter-active, .hex-keyboard-fade-leave-active {